From 5af8e0a646acdd22f693ce7451139a14446630ae Mon Sep 17 00:00:00 2001 From: Dmytro Kirpa Date: Thu, 26 Mar 2026 11:20:38 +0100 Subject: [PATCH 1/6] feat(react-toast): add base hooks for Toast, ToastTitle, and ToastBody Co-Authored-By: Claude Sonnet 4.6 --- change/@fluentui-react-toast-base-hooks.json | 7 ++ .../react-toast/library/src/Toast.ts | 10 ++- .../react-toast/library/src/ToastBody.ts | 9 ++- .../react-toast/library/src/ToastTitle.ts | 9 ++- .../src/components/Toast/Toast.types.ts | 12 +++- .../library/src/components/Toast/index.ts | 11 +++- .../library/src/components/Toast/useToast.ts | 28 +++++--- .../components/ToastBody/ToastBody.types.ts | 12 +++- .../library/src/components/ToastBody/index.ts | 10 ++- .../src/components/ToastBody/useToastBody.ts | 29 +++++++-- .../components/ToastTitle/ToastTitle.types.ts | 12 +++- .../src/components/ToastTitle/index.ts | 10 ++- .../components/ToastTitle/useToastTitle.tsx | 65 ++++++++++++------- .../react-toast/library/src/index.ts | 29 +++++++-- 14 files changed, 199 insertions(+), 54 deletions(-) create mode 100644 change/@fluentui-react-toast-base-hooks.json diff --git a/change/@fluentui-react-toast-base-hooks.json b/change/@fluentui-react-toast-base-hooks.json new file mode 100644 index 0000000000000..6bc15ab22f877 --- /dev/null +++ b/change/@fluentui-react-toast-base-hooks.json @@ -0,0 +1,7 @@ +{ + "type": "minor", + "comment": "feat: add useToastBase_unstable, useToastTitleBase_unstable, and useToastBodyBase_unstable hooks", + "packageName": "@fluentui/react-toast", + "email": "dmytrokirpa@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/packages/react-components/react-toast/library/src/Toast.ts b/packages/react-components/react-toast/library/src/Toast.ts index 13002c4a3b42e..f4bbcc0f1991e 100644 --- a/packages/react-components/react-toast/library/src/Toast.ts +++ b/packages/react-components/react-toast/library/src/Toast.ts @@ -1,8 +1,16 @@ -export type { ToastContextValues, ToastProps, ToastSlots, ToastState } from './components/Toast/index'; +export type { + ToastBaseProps, + ToastBaseState, + ToastContextValues, + ToastProps, + ToastSlots, + ToastState, +} from './components/Toast/index'; export { Toast, renderToast_unstable, toastClassNames, useToastStyles_unstable, + useToastBase_unstable, useToast_unstable, } from './components/Toast/index'; diff --git a/packages/react-components/react-toast/library/src/ToastBody.ts b/packages/react-components/react-toast/library/src/ToastBody.ts index 5ca80a167f342..7e8fd78fecfe0 100644 --- a/packages/react-components/react-toast/library/src/ToastBody.ts +++ b/packages/react-components/react-toast/library/src/ToastBody.ts @@ -1,8 +1,15 @@ -export type { ToastBodyProps, ToastBodySlots, ToastBodyState } from './components/ToastBody/index'; +export type { + ToastBodyBaseProps, + ToastBodyBaseState, + ToastBodyProps, + ToastBodySlots, + ToastBodyState, +} from './components/ToastBody/index'; export { ToastBody, renderToastBody_unstable, toastBodyClassNames, useToastBodyStyles_unstable, + useToastBodyBase_unstable, useToastBody_unstable, } from './components/ToastBody/index'; diff --git a/packages/react-components/react-toast/library/src/ToastTitle.ts b/packages/react-components/react-toast/library/src/ToastTitle.ts index 6765d6a8bc380..2fade006d768e 100644 --- a/packages/react-components/react-toast/library/src/ToastTitle.ts +++ b/packages/react-components/react-toast/library/src/ToastTitle.ts @@ -1,8 +1,15 @@ -export type { ToastTitleProps, ToastTitleSlots, ToastTitleState } from './components/ToastTitle/index'; +export type { + ToastTitleBaseProps, + ToastTitleBaseState, + ToastTitleProps, + ToastTitleSlots, + ToastTitleState, +} from './components/ToastTitle/index'; export { ToastTitle, renderToastTitle_unstable, toastTitleClassNames, useToastTitleStyles_unstable, + useToastTitleBase_unstable, useToastTitle_unstable, } from './components/ToastTitle/index'; diff --git a/packages/react-components/react-toast/library/src/components/Toast/Toast.types.ts b/packages/react-components/react-toast/library/src/components/Toast/Toast.types.ts index 5e418eb9f1771..83fa195add848 100644 --- a/packages/react-components/react-toast/library/src/components/Toast/Toast.types.ts +++ b/packages/react-components/react-toast/library/src/components/Toast/Toast.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 { BackgroundAppearanceContextValue } from '@fluentui/react-shared-contexts'; import type { ToastIntent } from '../../state/types'; @@ -17,6 +17,11 @@ export type ToastProps = ComponentProps & { appearance?: BackgroundAppearanceContextValue; }; +/** + * Toast Props without design-only props. + */ +export type ToastBaseProps = DistributiveOmit; + /** * State used in rendering Toast */ @@ -24,3 +29,8 @@ export type ToastState = ComponentState & { backgroundAppearance: BackgroundAppearanceContextValue; intent?: ToastIntent | undefined; }; + +/** + * State used in rendering Toast, without design-only state. + */ +export type ToastBaseState = DistributiveOmit; diff --git a/packages/react-components/react-toast/library/src/components/Toast/index.ts b/packages/react-components/react-toast/library/src/components/Toast/index.ts index 1e9d2f2c99231..d1d5f897e33d0 100644 --- a/packages/react-components/react-toast/library/src/components/Toast/index.ts +++ b/packages/react-components/react-toast/library/src/components/Toast/index.ts @@ -1,5 +1,12 @@ export { Toast } from './Toast'; -export type { ToastContextValues, ToastProps, ToastSlots, ToastState } from './Toast.types'; +export type { + ToastBaseProps, + ToastBaseState, + ToastContextValues, + ToastProps, + ToastSlots, + ToastState, +} from './Toast.types'; export { renderToast_unstable } from './renderToast'; -export { useToast_unstable } from './useToast'; +export { useToastBase_unstable, useToast_unstable } from './useToast'; export { toastClassNames, useToastStyles_unstable } from './useToastStyles.styles'; diff --git a/packages/react-components/react-toast/library/src/components/Toast/useToast.ts b/packages/react-components/react-toast/library/src/components/Toast/useToast.ts index 4f5c05f3f319f..cea138f29fff2 100644 --- a/packages/react-components/react-toast/library/src/components/Toast/useToast.ts +++ b/packages/react-components/react-toast/library/src/components/Toast/useToast.ts @@ -2,19 +2,16 @@ import * as React from 'react'; import { getIntrinsicElementProps, slot } from '@fluentui/react-utilities'; -import type { ToastProps, ToastState } from './Toast.types'; +import type { ToastBaseProps, ToastBaseState, ToastProps, ToastState } from './Toast.types'; import { useToastContainerContext } from '../../contexts/toastContainerContext'; /** - * Create the state required to render Toast. - * - * The returned state can be modified with hooks such as useToastStyles_unstable, - * before being passed to renderToast_unstable. + * Create the base state required to render Toast, without design-only props. * - * @param props - props from this instance of Toast + * @param props - props from this instance of Toast (without appearance) * @param ref - reference to root HTMLElement of Toast */ -export const useToast_unstable = (props: ToastProps, ref: React.Ref): ToastState => { +export const useToastBase_unstable = (props: ToastBaseProps, ref: React.Ref): ToastBaseState => { const { intent } = useToastContainerContext(); return { @@ -31,7 +28,22 @@ export const useToast_unstable = (props: ToastProps, ref: React.Ref }), { elementType: 'div' }, ), - backgroundAppearance: props.appearance, intent, }; }; + +/** + * Create the state required to render Toast. + * + * The returned state can be modified with hooks such as useToastStyles_unstable, + * before being passed to renderToast_unstable. + * + * @param props - props from this instance of Toast + * @param ref - reference to root HTMLElement of Toast + */ +export const useToast_unstable = (props: ToastProps, ref: React.Ref): ToastState => { + return { + ...useToastBase_unstable(props, ref), + backgroundAppearance: props.appearance, + }; +}; diff --git a/packages/react-components/react-toast/library/src/components/ToastBody/ToastBody.types.ts b/packages/react-components/react-toast/library/src/components/ToastBody/ToastBody.types.ts index 570330970d2e7..e1d695d6bf303 100644 --- a/packages/react-components/react-toast/library/src/components/ToastBody/ToastBody.types.ts +++ b/packages/react-components/react-toast/library/src/components/ToastBody/ToastBody.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 { BackgroundAppearanceContextValue } from '@fluentui/react-shared-contexts'; export type ToastBodySlots = { @@ -11,9 +11,19 @@ export type ToastBodySlots = { */ export type ToastBodyProps = ComponentProps & {}; +/** + * ToastBody Props without design-only props. + */ +export type ToastBodyBaseProps = ToastBodyProps; + /** * State used in rendering ToastBody */ export type ToastBodyState = ComponentState & { backgroundAppearance: BackgroundAppearanceContextValue; }; + +/** + * State used in rendering ToastBody, without design-only state. + */ +export type ToastBodyBaseState = DistributiveOmit; diff --git a/packages/react-components/react-toast/library/src/components/ToastBody/index.ts b/packages/react-components/react-toast/library/src/components/ToastBody/index.ts index 2903dea7132ad..9de88b2dcff83 100644 --- a/packages/react-components/react-toast/library/src/components/ToastBody/index.ts +++ b/packages/react-components/react-toast/library/src/components/ToastBody/index.ts @@ -1,5 +1,11 @@ export { ToastBody } from './ToastBody'; -export type { ToastBodyProps, ToastBodySlots, ToastBodyState } from './ToastBody.types'; +export type { + ToastBodyBaseProps, + ToastBodyBaseState, + ToastBodyProps, + ToastBodySlots, + ToastBodyState, +} from './ToastBody.types'; export { renderToastBody_unstable } from './renderToastBody'; -export { useToastBody_unstable } from './useToastBody'; +export { useToastBodyBase_unstable, useToastBody_unstable } from './useToastBody'; export { toastBodyClassNames, useToastBodyStyles_unstable } from './useToastBodyStyles.styles'; diff --git a/packages/react-components/react-toast/library/src/components/ToastBody/useToastBody.ts b/packages/react-components/react-toast/library/src/components/ToastBody/useToastBody.ts index 08466d824d9f8..aac4480f68d63 100644 --- a/packages/react-components/react-toast/library/src/components/ToastBody/useToastBody.ts +++ b/packages/react-components/react-toast/library/src/components/ToastBody/useToastBody.ts @@ -2,21 +2,20 @@ import * as React from 'react'; import { getIntrinsicElementProps, slot } from '@fluentui/react-utilities'; -import type { ToastBodyProps, ToastBodyState } from './ToastBody.types'; +import type { ToastBodyBaseProps, ToastBodyBaseState, ToastBodyProps, ToastBodyState } from './ToastBody.types'; import { useToastContainerContext } from '../../contexts/toastContainerContext'; import { useBackgroundAppearance } from '@fluentui/react-shared-contexts'; /** - * Create the state required to render ToastBody. - * - * The returned state can be modified with hooks such as useToastBodyStyles_unstable, - * before being passed to renderToastBody_unstable. + * Create the base state required to render ToastBody, without design-only props. * * @param props - props from this instance of ToastBody * @param ref - reference to root HTMLElement of ToastBody */ -export const useToastBody_unstable = (props: ToastBodyProps, ref: React.Ref): ToastBodyState => { - const backgroundAppearance = useBackgroundAppearance(); +export const useToastBodyBase_unstable = ( + props: ToastBodyBaseProps, + ref: React.Ref, +): ToastBodyBaseState => { const { bodyId } = useToastContainerContext(); return { components: { @@ -35,6 +34,22 @@ export const useToastBody_unstable = (props: ToastBodyProps, ref: React.Ref): ToastBodyState => { + const backgroundAppearance = useBackgroundAppearance(); + return { + ...useToastBodyBase_unstable(props, ref), backgroundAppearance, }; }; diff --git a/packages/react-components/react-toast/library/src/components/ToastTitle/ToastTitle.types.ts b/packages/react-components/react-toast/library/src/components/ToastTitle/ToastTitle.types.ts index 6b4676098291e..595fb8ef4fdbb 100644 --- a/packages/react-components/react-toast/library/src/components/ToastTitle/ToastTitle.types.ts +++ b/packages/react-components/react-toast/library/src/components/ToastTitle/ToastTitle.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 { BackgroundAppearanceContextValue } from '@fluentui/react-shared-contexts'; import { ToastContainerContextValue } from '../../contexts/toastContainerContext'; @@ -13,6 +13,11 @@ export type ToastTitleSlots = { */ export type ToastTitleProps = ComponentProps & {}; +/** + * ToastTitle Props without design-only props. + */ +export type ToastTitleBaseProps = ToastTitleProps; + /** * State used in rendering ToastTitle */ @@ -20,3 +25,8 @@ export type ToastTitleState = ComponentState & Pick & { backgroundAppearance: BackgroundAppearanceContextValue; }; + +/** + * State used in rendering ToastTitle, without design-only state. + */ +export type ToastTitleBaseState = DistributiveOmit; diff --git a/packages/react-components/react-toast/library/src/components/ToastTitle/index.ts b/packages/react-components/react-toast/library/src/components/ToastTitle/index.ts index 6d187294dcfd2..5ed85bfa1be39 100644 --- a/packages/react-components/react-toast/library/src/components/ToastTitle/index.ts +++ b/packages/react-components/react-toast/library/src/components/ToastTitle/index.ts @@ -1,5 +1,11 @@ export { ToastTitle } from './ToastTitle'; -export type { ToastTitleProps, ToastTitleSlots, ToastTitleState } from './ToastTitle.types'; +export type { + ToastTitleBaseProps, + ToastTitleBaseState, + ToastTitleProps, + ToastTitleSlots, + ToastTitleState, +} from './ToastTitle.types'; export { renderToastTitle_unstable } from './renderToastTitle'; -export { useToastTitle_unstable } from './useToastTitle'; +export { useToastTitleBase_unstable, useToastTitle_unstable } from './useToastTitle'; export { toastTitleClassNames, useToastTitleStyles_unstable } from './useToastTitleStyles.styles'; diff --git a/packages/react-components/react-toast/library/src/components/ToastTitle/useToastTitle.tsx b/packages/react-components/react-toast/library/src/components/ToastTitle/useToastTitle.tsx index a0416ad8dc53f..04775ba73ab64 100644 --- a/packages/react-components/react-toast/library/src/components/ToastTitle/useToastTitle.tsx +++ b/packages/react-components/react-toast/library/src/components/ToastTitle/useToastTitle.tsx @@ -6,9 +6,44 @@ import { CheckmarkCircleFilled, DiamondDismissFilled, InfoFilled, WarningFilled import { getIntrinsicElementProps, slot } from '@fluentui/react-utilities'; import { useBackgroundAppearance } from '@fluentui/react-shared-contexts'; -import type { ToastTitleProps, ToastTitleState } from './ToastTitle.types'; +import type { ToastTitleBaseProps, ToastTitleBaseState, ToastTitleProps, ToastTitleState } from './ToastTitle.types'; import { useToastContainerContext } from '../../contexts/toastContainerContext'; +/** + * Create the base state required to render ToastTitle, without design-only props. + * + * @param props - props from this instance of ToastTitle + * @param ref - reference to root HTMLElement of ToastTitle + */ +export const useToastTitleBase_unstable = ( + props: ToastTitleBaseProps, + ref: React.Ref, +): ToastTitleBaseState => { + const { intent, titleId } = useToastContainerContext(); + + return { + action: slot.optional(props.action, { elementType: 'div' }), + components: { root: 'div', media: 'div', action: 'div' }, + media: slot.optional(props.media, { + renderByDefault: !!intent, + elementType: 'div', + }), + root: slot.always( + getIntrinsicElementProps('div', { + // FIXME: + // `ref` 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: ref as React.Ref, + children: props.children, + id: titleId, + ...props, + }), + { elementType: 'div' }, + ), + intent, + }; +}; + /** * Create the state required to render ToastTitle. * @@ -19,12 +54,12 @@ import { useToastContainerContext } from '../../contexts/toastContainerContext'; * @param ref - reference to root HTMLElement of ToastTitle */ export const useToastTitle_unstable = (props: ToastTitleProps, ref: React.Ref): ToastTitleState => { - const { intent, titleId } = useToastContainerContext(); const backgroundAppearance = useBackgroundAppearance(); + const baseState = useToastTitleBase_unstable(props, ref); /** Determine the role and media to render based on the intent */ let defaultIcon; - switch (intent) { + switch (baseState.intent) { case 'success': defaultIcon = ; break; @@ -40,26 +75,10 @@ export const useToastTitle_unstable = (props: ToastTitleProps, ref: React.Ref, - children: props.children, - id: titleId, - ...props, - }), - { elementType: 'div' }, - ), - intent, + ...baseState, backgroundAppearance, + media: baseState.media + ? { ...baseState.media, children: baseState.media.children ?? defaultIcon } + : baseState.media, }; }; diff --git a/packages/react-components/react-toast/library/src/index.ts b/packages/react-components/react-toast/library/src/index.ts index 3e57242a6cefe..d436993d6b0a3 100644 --- a/packages/react-components/react-toast/library/src/index.ts +++ b/packages/react-components/react-toast/library/src/index.ts @@ -11,26 +11,47 @@ export { toasterClassNames, } from './Toaster'; export type { ToasterProps, ToasterState, ToasterSlots } from './Toaster'; -export { Toast, useToastStyles_unstable, useToast_unstable, renderToast_unstable, toastClassNames } from './Toast'; -export type { ToastProps, ToastState, ToastSlots } from './Toast'; +export { + Toast, + useToastStyles_unstable, + useToastBase_unstable, + useToast_unstable, + renderToast_unstable, + toastClassNames, +} from './Toast'; +export type { ToastBaseProps, ToastBaseState, ToastProps, ToastState, ToastSlots } from './Toast'; export { ToastTitle, useToastTitleStyles_unstable, + useToastTitleBase_unstable, useToastTitle_unstable, renderToastTitle_unstable, toastTitleClassNames, } from './ToastTitle'; -export type { ToastTitleProps, ToastTitleState, ToastTitleSlots } from './ToastTitle'; +export type { + ToastTitleBaseProps, + ToastTitleBaseState, + ToastTitleProps, + ToastTitleState, + ToastTitleSlots, +} from './ToastTitle'; export { ToastBody, useToastBodyStyles_unstable, + useToastBodyBase_unstable, useToastBody_unstable, renderToastBody_unstable, toastBodyClassNames, } from './ToastBody'; -export type { ToastBodyProps, ToastBodyState, ToastBodySlots } from './ToastBody'; +export type { + ToastBodyBaseProps, + ToastBodyBaseState, + ToastBodyProps, + ToastBodyState, + ToastBodySlots, +} from './ToastBody'; export { ToastFooter, From ffdcb44f2e66ea40bd066d5703c5bd0b55f6af86 Mon Sep 17 00:00:00 2001 From: Dmytro Kirpa Date: Sun, 29 Mar 2026 19:23:58 +0200 Subject: [PATCH 2/6] fixup --- .../library/etc/react-toast.api.md | 27 +++++++++++++++++++ .../src/components/Toast/Toast.types.ts | 6 ++--- .../library/src/components/Toast/useToast.ts | 3 ++- .../components/ToastBody/ToastBody.types.ts | 4 +-- .../components/ToastTitle/ToastTitle.types.ts | 4 +-- 5 files changed, 36 insertions(+), 8 deletions(-) diff --git a/packages/react-components/react-toast/library/etc/react-toast.api.md b/packages/react-components/react-toast/library/etc/react-toast.api.md index 206e52e983db9..c274801150b65 100644 --- a/packages/react-components/react-toast/library/etc/react-toast.api.md +++ b/packages/react-components/react-toast/library/etc/react-toast.api.md @@ -38,9 +38,21 @@ export const renderToastTrigger_unstable: (state: ToastTriggerState) => JSXEleme // @public export const Toast: ForwardRefComponent; +// @public +export type ToastBaseProps = Omit; + +// @public +export type ToastBaseState = Omit; + // @public export const ToastBody: ForwardRefComponent; +// @public +export type ToastBodyBaseProps = ToastBodyProps; + +// @public +export type ToastBodyBaseState = Omit; + // @public (undocumented) export const toastBodyClassNames: SlotClassNames; @@ -154,6 +166,12 @@ export type ToastStatus = 'queued' | 'visible' | 'dismissed' | 'unmounted'; // @public export const ToastTitle: ForwardRefComponent; +// @public +export type ToastTitleBaseProps = ToastTitleProps; + +// @public +export type ToastTitleBaseState = Omit; + // @public (undocumented) export const toastTitleClassNames: SlotClassNames; @@ -191,9 +209,15 @@ export type ToastTriggerState = { // @public export const useToast_unstable: (props: ToastProps, ref: React_2.Ref) => ToastState; +// @public +export const useToastBase_unstable: (props: ToastBaseProps, ref: React_2.Ref) => ToastBaseState; + // @public export const useToastBody_unstable: (props: ToastBodyProps, ref: React_2.Ref) => ToastBodyState; +// @public +export const useToastBodyBase_unstable: (props: ToastBodyBaseProps, ref: React_2.Ref) => ToastBodyBaseState; + // @public export const useToastBodyStyles_unstable: (state: ToastBodyState) => ToastBodyState; @@ -225,6 +249,9 @@ export const useToastStyles_unstable: (state: ToastState) => ToastState; // @public export const useToastTitle_unstable: (props: ToastTitleProps, ref: React_2.Ref) => ToastTitleState; +// @public +export const useToastTitleBase_unstable: (props: ToastTitleBaseProps, ref: React_2.Ref) => ToastTitleBaseState; + // @public export const useToastTitleStyles_unstable: (state: ToastTitleState) => ToastTitleState; diff --git a/packages/react-components/react-toast/library/src/components/Toast/Toast.types.ts b/packages/react-components/react-toast/library/src/components/Toast/Toast.types.ts index 83fa195add848..684cee44828f3 100644 --- a/packages/react-components/react-toast/library/src/components/Toast/Toast.types.ts +++ b/packages/react-components/react-toast/library/src/components/Toast/Toast.types.ts @@ -1,4 +1,4 @@ -import type { ComponentProps, ComponentState, DistributiveOmit, Slot } from '@fluentui/react-utilities'; +import type { ComponentProps, ComponentState, Slot } from '@fluentui/react-utilities'; import { BackgroundAppearanceContextValue } from '@fluentui/react-shared-contexts'; import type { ToastIntent } from '../../state/types'; @@ -20,7 +20,7 @@ export type ToastProps = ComponentProps & { /** * Toast Props without design-only props. */ -export type ToastBaseProps = DistributiveOmit; +export type ToastBaseProps = Omit; /** * State used in rendering Toast @@ -33,4 +33,4 @@ export type ToastState = ComponentState & { /** * State used in rendering Toast, without design-only state. */ -export type ToastBaseState = DistributiveOmit; +export type ToastBaseState = Omit; diff --git a/packages/react-components/react-toast/library/src/components/Toast/useToast.ts b/packages/react-components/react-toast/library/src/components/Toast/useToast.ts index cea138f29fff2..e55736f840c41 100644 --- a/packages/react-components/react-toast/library/src/components/Toast/useToast.ts +++ b/packages/react-components/react-toast/library/src/components/Toast/useToast.ts @@ -42,8 +42,9 @@ export const useToastBase_unstable = (props: ToastBaseProps, ref: React.Ref): ToastState => { + const state = useToastBase_unstable(props, ref); return { - ...useToastBase_unstable(props, ref), + ...state, backgroundAppearance: props.appearance, }; }; diff --git a/packages/react-components/react-toast/library/src/components/ToastBody/ToastBody.types.ts b/packages/react-components/react-toast/library/src/components/ToastBody/ToastBody.types.ts index e1d695d6bf303..70936c45b3dea 100644 --- a/packages/react-components/react-toast/library/src/components/ToastBody/ToastBody.types.ts +++ b/packages/react-components/react-toast/library/src/components/ToastBody/ToastBody.types.ts @@ -1,4 +1,4 @@ -import type { ComponentProps, ComponentState, DistributiveOmit, Slot } from '@fluentui/react-utilities'; +import type { ComponentProps, ComponentState, Slot } from '@fluentui/react-utilities'; import { BackgroundAppearanceContextValue } from '@fluentui/react-shared-contexts'; export type ToastBodySlots = { @@ -26,4 +26,4 @@ export type ToastBodyState = ComponentState & { /** * State used in rendering ToastBody, without design-only state. */ -export type ToastBodyBaseState = DistributiveOmit; +export type ToastBodyBaseState = Omit; diff --git a/packages/react-components/react-toast/library/src/components/ToastTitle/ToastTitle.types.ts b/packages/react-components/react-toast/library/src/components/ToastTitle/ToastTitle.types.ts index 595fb8ef4fdbb..8579c5d7196a8 100644 --- a/packages/react-components/react-toast/library/src/components/ToastTitle/ToastTitle.types.ts +++ b/packages/react-components/react-toast/library/src/components/ToastTitle/ToastTitle.types.ts @@ -1,4 +1,4 @@ -import type { ComponentProps, ComponentState, DistributiveOmit, Slot } from '@fluentui/react-utilities'; +import type { ComponentProps, ComponentState, Slot } from '@fluentui/react-utilities'; import { BackgroundAppearanceContextValue } from '@fluentui/react-shared-contexts'; import { ToastContainerContextValue } from '../../contexts/toastContainerContext'; @@ -29,4 +29,4 @@ export type ToastTitleState = ComponentState & /** * State used in rendering ToastTitle, without design-only state. */ -export type ToastTitleBaseState = DistributiveOmit; +export type ToastTitleBaseState = Omit; From eccfc97dfbc16acc3889f7d195bc8e2f5ec31175 Mon Sep 17 00:00:00 2001 From: Dmytro Kirpa Date: Thu, 14 May 2026 12:05:06 +0200 Subject: [PATCH 3/6] feat(react-headless-components-preview): add Toast component (cherry-picked from cb4597460d) --- .../library/etc/react-toast.api.md | 51 +++++++++++++++++++ .../react-toast/library/src/index.ts | 18 ++++++- 2 files changed, 67 insertions(+), 2 deletions(-) diff --git a/packages/react-components/react-toast/library/etc/react-toast.api.md b/packages/react-components/react-toast/library/etc/react-toast.api.md index c274801150b65..e66d26104daa4 100644 --- a/packages/react-components/react-toast/library/etc/react-toast.api.md +++ b/packages/react-components/react-toast/library/etc/react-toast.api.md @@ -17,6 +17,12 @@ import type { Slot } from '@fluentui/react-utilities'; import type { SlotClassNames } from '@fluentui/react-utilities'; import type { TriggerProps } from '@fluentui/react-utilities'; +// @public (undocumented) +export interface DispatchToastOptions extends Partial> { + // (undocumented) + root?: RootSlot; +} + // @public export const renderToast_unstable: (state: ToastState, contextValues: ToastContextValues) => JSXElement; @@ -70,6 +76,15 @@ export type ToastBodyState = ComponentState & { backgroundAppearance: BackgroundAppearanceContextValue; }; +// @public (undocumented) +export interface ToastChangeData extends ToastOptions, Pick { + // (undocumented) + status: ToastStatus; +} + +// @public (undocumented) +export type ToastChangeHandler = (event: null, data: ToastChangeData) => void; + // @public (undocumented) export const toastClassNames: SlotClassNames; @@ -88,12 +103,25 @@ export type ToastContainerState = ComponentState & Pick void; }; +// @public (undocumented) +export interface ToastData extends ToastOptions { + close: () => void; + // (undocumented) + imperativeRef: React_2.RefObject; + order: number; + remove: () => void; + updateId: number; +} + // @public export const Toaster: React_2.FC; // @public (undocumented) export const toasterClassNames: SlotClassNames; +// @public (undocumented) +export type ToasterId = string; + // @public export type ToasterProps = Omit, 'children'> & Partial & Pick & { announce?: Announce; @@ -132,6 +160,13 @@ export type ToastFooterState = ComponentState; // @public (undocumented) export type ToastId = string; +// @public (undocumented) +export type ToastImperativeRef = { + focus: () => void; + play: () => void; + pause: () => void; +}; + // @public (undocumented) export type ToastIntent = 'info' | 'success' | 'error' | 'warning'; @@ -206,6 +241,12 @@ export type ToastTriggerState = { children: React_2.ReactElement | null; }; +// @public (undocumented) +export interface UpdateToastOptions extends UpdateToastEventDetail { + // (undocumented) + root?: RootSlot; +} + // @public export const useToast_unstable: (props: ToastProps, ref: React_2.Ref) => ToastState; @@ -231,6 +272,16 @@ export function useToastController(toasterId?: ToasterId): { playToast: (toastId: ToastId) => void; }; +// @public (undocumented) +export function useToaster(options?: Partial): { + isToastVisible: (toastId: ToastId) => boolean; + toastsToRender: Map; + pauseAllToasts: () => void; + playAllToasts: () => void; + tryRestoreFocus: () => void; + closeAllToasts: () => void; +}; + // @public export const useToaster_unstable: (props: ToasterProps) => ToasterState; diff --git a/packages/react-components/react-toast/library/src/index.ts b/packages/react-components/react-toast/library/src/index.ts index d436993d6b0a3..7ee9e13581d1d 100644 --- a/packages/react-components/react-toast/library/src/index.ts +++ b/packages/react-components/react-toast/library/src/index.ts @@ -1,5 +1,19 @@ -export { useToastController } from './state'; -export type { ToastPosition, ToastId, ToastOffset, ToastPoliteness, ToastStatus, ToastIntent } from './state'; +export { useToastController, useToaster } from './state'; +export type { + ToastPosition, + ToastId, + ToastOffset, + ToastPoliteness, + ToastStatus, + ToastIntent, + ToasterId, + ToastImperativeRef, + Toast as ToastData, + ToastChangeData, + ToastChangeHandler, + DispatchToastOptions, + UpdateToastOptions, +} from './state'; export { ToastTrigger, useToastTrigger_unstable, renderToastTrigger_unstable } from './ToastTrigger'; export type { ToastTriggerChildProps, ToastTriggerProps, ToastTriggerState } from './ToastTrigger'; From 217f07a54e5fccaacf29076fcd1f4df7de5a6ddf Mon Sep 17 00:00:00 2001 From: Dmytro Kirpa Date: Thu, 14 May 2026 12:05:06 +0200 Subject: [PATCH 4/6] update toaster components, add tests, improve stories (cherry-picked from af7a2459c1) --- .../library/etc/react-toast.api.md | 79 +++++++++++++++++++ .../react-toast/library/src/Toaster.ts | 1 + .../library/src/components/Toaster/index.ts | 1 + .../react-toast/library/src/index.ts | 13 ++- 4 files changed, 93 insertions(+), 1 deletion(-) diff --git a/packages/react-components/react-toast/library/etc/react-toast.api.md b/packages/react-components/react-toast/library/etc/react-toast.api.md index e66d26104daa4..79be99b1598d3 100644 --- a/packages/react-components/react-toast/library/etc/react-toast.api.md +++ b/packages/react-components/react-toast/library/etc/react-toast.api.md @@ -17,18 +17,39 @@ import type { Slot } from '@fluentui/react-utilities'; import type { SlotClassNames } from '@fluentui/react-utilities'; import type { TriggerProps } from '@fluentui/react-utilities'; +// @public (undocumented) +export type Announce = (message: string, options: AnnounceOptions) => void; + +// @public (undocumented) +export type AnnounceOptions = { + politeness: AriaLivePoliteness; +}; + +// @public (undocumented) +export type AriaLivePoliteness = 'polite' | 'assertive'; + // @public (undocumented) export interface DispatchToastOptions extends Partial> { // (undocumented) root?: RootSlot; } +// @public (undocumented) +export type LiveMessage = { + message: string; + createdAt: number; + politeness: AriaLivePoliteness; +}; + // @public export const renderToast_unstable: (state: ToastState, contextValues: ToastContextValues) => JSXElement; // @public export const renderToastBody_unstable: (state: ToastBodyState) => JSXElement; +// @public +export const renderToastContainer_unstable: (state: ToastContainerState, contextValues: ToastContainerContextValues) => JSXElement; + // @public export const renderToaster_unstable: (state: ToasterState) => JSXElement; @@ -91,6 +112,31 @@ export const toastClassNames: SlotClassNames; // @public (undocumented) export const toastContainerClassNames: SlotClassNames; +// @public (undocumented) +export const ToastContainerContextProvider: React_2.Provider; + +// @public (undocumented) +export type ToastContainerContextValue = { + close: () => void; + intent: ToastIntent | undefined; + bodyId: string; + titleId: string; +}; + +// @public +export type ToastContainerProps = Omit>, 'content'> & ToastData & { + visible: boolean; + announce: Announce; + intent: ToastIntent | undefined; + tryRestoreFocus: () => void; +}; + +// @public (undocumented) +export type ToastContainerSlots = { + root: NonNullable>; + timer: NonNullable>; +}; + // @public export type ToastContainerState = ComponentState & Pick & Pick & { transitionTimeout: number; @@ -122,12 +168,30 @@ export const toasterClassNames: SlotClassNames; // @public (undocumented) export type ToasterId = string; +// @public (undocumented) +export interface ToasterOptions extends Pick { + // (undocumented) + limit?: number; + // (undocumented) + offset?: ToastOffset; + // (undocumented) + shortcuts?: ToasterShortcuts; + // (undocumented) + toasterId?: ToasterId; +} + // @public export type ToasterProps = Omit, 'children'> & Partial & Pick & { announce?: Announce; inline?: boolean; }; +// @public (undocumented) +export interface ToasterShortcuts { + // (undocumented) + focus: (e: KeyboardEvent) => boolean; +} + // @public (undocumented) export type ToasterSlots = { root: Slot<'div'>; @@ -250,6 +314,12 @@ export interface UpdateToastOptions extends UpdateToastEventDetail { // @public export const useToast_unstable: (props: ToastProps, ref: React_2.Ref) => ToastState; +// @public +export function useToastAnnounce(announce: Announce): { + announceToast: Announce; + toasterRef: React_2.RefCallback; +}; + // @public export const useToastBase_unstable: (props: ToastBaseProps, ref: React_2.Ref) => ToastBaseState; @@ -262,6 +332,15 @@ export const useToastBodyBase_unstable: (props: ToastBodyBaseProps, ref: React_2 // @public export const useToastBodyStyles_unstable: (state: ToastBodyState) => ToastBodyState; +// @public +export const useToastContainer_unstable: (props: ToastContainerProps, ref: React_2.Ref) => ToastContainerState; + +// @public (undocumented) +export const useToastContainerContext: () => ToastContainerContextValue; + +// @public (undocumented) +export function useToastContainerContextValues_unstable(state: ToastContainerState): ToastContainerContextValues; + // @public (undocumented) export function useToastController(toasterId?: ToasterId): { dispatchToast: (content: React_2.ReactNode, options?: DispatchToastOptions) => void; diff --git a/packages/react-components/react-toast/library/src/Toaster.ts b/packages/react-components/react-toast/library/src/Toaster.ts index 340bda3b7a4aa..10baaa4b48303 100644 --- a/packages/react-components/react-toast/library/src/Toaster.ts +++ b/packages/react-components/react-toast/library/src/Toaster.ts @@ -5,4 +5,5 @@ export { toasterClassNames, useToasterStyles_unstable, useToaster_unstable, + useToastAnnounce, } from './components/Toaster/index'; diff --git a/packages/react-components/react-toast/library/src/components/Toaster/index.ts b/packages/react-components/react-toast/library/src/components/Toaster/index.ts index 17a0deb7f8eb8..10c4dc591ec1d 100644 --- a/packages/react-components/react-toast/library/src/components/Toaster/index.ts +++ b/packages/react-components/react-toast/library/src/components/Toaster/index.ts @@ -3,3 +3,4 @@ export type { ToasterProps, ToasterSlots, ToasterSlotsInternal, ToasterState } f export { renderToaster_unstable } from './renderToaster'; export { useToaster_unstable } from './useToaster'; export { toasterClassNames, useToasterStyles_unstable } from './useToasterStyles.styles'; +export { useToastAnnounce } from './useToastAnnounce'; diff --git a/packages/react-components/react-toast/library/src/index.ts b/packages/react-components/react-toast/library/src/index.ts index 7ee9e13581d1d..6e8c3b90df94a 100644 --- a/packages/react-components/react-toast/library/src/index.ts +++ b/packages/react-components/react-toast/library/src/index.ts @@ -1,4 +1,5 @@ export { useToastController, useToaster } from './state'; +export type { Announce, AnnounceOptions, AriaLivePoliteness, LiveMessage } from './AriaLive'; export type { ToastPosition, ToastId, @@ -7,6 +8,8 @@ export type { ToastStatus, ToastIntent, ToasterId, + ToasterOptions, + ToasterShortcuts, ToastImperativeRef, Toast as ToastData, ToastChangeData, @@ -23,6 +26,7 @@ export { useToasterStyles_unstable, renderToaster_unstable, toasterClassNames, + useToastAnnounce, } from './Toaster'; export type { ToasterProps, ToasterState, ToasterSlots } from './Toaster'; export { @@ -76,5 +80,12 @@ export { } from './ToastFooter'; export type { ToastFooterProps, ToastFooterState, ToastFooterSlots } from './ToastFooter'; +export type { ToastContainerContextValue } from './contexts/toastContainerContext'; +export { ToastContainerContextProvider, useToastContainerContext } from './contexts/toastContainerContext'; export { toastContainerClassNames } from './ToastContainer'; -export type { ToastContainerState } from './ToastContainer'; +export type { ToastContainerSlots, ToastContainerProps, ToastContainerState } from './ToastContainer'; +export { + useToastContainer_unstable, + renderToastContainer_unstable, + useToastContainerContextValues_unstable, +} from './ToastContainer'; From 0556c5645e282236e950868aa326892653046030 Mon Sep 17 00:00:00 2001 From: Dmytro Kirpa Date: Thu, 14 May 2026 13:32:41 +0200 Subject: [PATCH 5/6] fix api-extractor warning --- .../library/src/components/Toaster/useToastAnnounce.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react-components/react-toast/library/src/components/Toaster/useToastAnnounce.ts b/packages/react-components/react-toast/library/src/components/Toaster/useToastAnnounce.ts index 9f1fa23b35fd1..091e525ab0910 100644 --- a/packages/react-components/react-toast/library/src/components/Toaster/useToastAnnounce.ts +++ b/packages/react-components/react-toast/library/src/components/Toaster/useToastAnnounce.ts @@ -8,7 +8,7 @@ import { isHTMLElement } from '@fluentui/react-utilities'; * Wraps an aria live announcement function. * Aria live announcements can be detrimental once the user is already navigating * multiple toasts. Once the user is focused inside the toaster, the announecments should be disabled. - * @param announce + * @param announce - The aria live announcement function to wrap * @returns A function to announce a toast and a ref to attach to the toaster element */ export function useToastAnnounce(announce: Announce): { From 79afd1a05891b247ee6d05e9a2c3f705ad82cb4e Mon Sep 17 00:00:00 2001 From: Dmytro Kirpa Date: Thu, 14 May 2026 15:51:50 +0200 Subject: [PATCH 6/6] fix(react-toast): expose transitively-referenced types in public API Inline RootSlot as Slot<'div'> in DispatchToastOptions/UpdateToastOptions and export ToastOptions and UpdateToastEventDetail so the api-extractor report no longer references undeclared private types. Also suppress react-compiler on useToastTitle_unstable to allow inline mutation of the media slot's default icon. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../library/etc/react-toast.api.md | 26 +++++++++++++++++-- .../components/ToastTitle/useToastTitle.tsx | 10 ++++--- .../react-toast/library/src/index.ts | 2 ++ .../react-toast/library/src/state/types.ts | 6 ++--- 4 files changed, 35 insertions(+), 9 deletions(-) diff --git a/packages/react-components/react-toast/library/etc/react-toast.api.md b/packages/react-components/react-toast/library/etc/react-toast.api.md index 39c96c6f87838..f460c374c5158 100644 --- a/packages/react-components/react-toast/library/etc/react-toast.api.md +++ b/packages/react-components/react-toast/library/etc/react-toast.api.md @@ -31,7 +31,7 @@ export type AriaLivePoliteness = 'polite' | 'assertive'; // @public (undocumented) export interface DispatchToastOptions extends Partial> { // (undocumented) - root?: RootSlot; + root?: Slot<'div'>; } // @public (undocumented) @@ -237,6 +237,22 @@ export type ToastIntent = 'info' | 'success' | 'error' | 'warning'; // @public (undocumented) export type ToastOffset = Partial> | ToastOffsetObject; +// @public (undocumented) +export interface ToastOptions { + content: unknown; + data: TData; + intent?: ToastIntent; + onStatusChange: ToastChangeHandler | undefined; + pauseOnHover: boolean; + pauseOnWindowBlur: boolean; + politeness?: ToastPoliteness; + position: ToastPosition; + priority: number; + timeout: number; + toasterId: ToasterId | undefined; + toastId: ToastId; +} + // @public (undocumented) export type ToastPoliteness = 'assertive' | 'polite'; @@ -305,10 +321,16 @@ export type ToastTriggerState = { children: React_2.ReactElement | null; }; +// @public (undocumented) +export interface UpdateToastEventDetail extends Partial, CommonToastDetail { + // (undocumented) + toastId: ToastId; +} + // @public (undocumented) export interface UpdateToastOptions extends UpdateToastEventDetail { // (undocumented) - root?: RootSlot; + root?: Slot<'div'>; } // @public diff --git a/packages/react-components/react-toast/library/src/components/ToastTitle/useToastTitle.tsx b/packages/react-components/react-toast/library/src/components/ToastTitle/useToastTitle.tsx index 04775ba73ab64..5be1d79ad4ee4 100644 --- a/packages/react-components/react-toast/library/src/components/ToastTitle/useToastTitle.tsx +++ b/packages/react-components/react-toast/library/src/components/ToastTitle/useToastTitle.tsx @@ -54,6 +54,8 @@ export const useToastTitleBase_unstable = ( * @param ref - reference to root HTMLElement of ToastTitle */ export const useToastTitle_unstable = (props: ToastTitleProps, ref: React.Ref): ToastTitleState => { + 'use no memo'; + const backgroundAppearance = useBackgroundAppearance(); const baseState = useToastTitleBase_unstable(props, ref); @@ -74,11 +76,13 @@ export const useToastTitle_unstable = (props: ToastTitleProps, ref: React.Ref; }; -type RootSlot = Slot<'div'>; - export interface DispatchToastOptions extends Partial> { - root?: RootSlot; + root?: Slot<'div'>; } export interface UpdateToastOptions extends UpdateToastEventDetail { - root?: RootSlot; + root?: Slot<'div'>; } export type ToastImperativeRef = {