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 1f12295646cc6f..85dbe3390b3807 100644 --- a/packages/react-components/react-dialog/etc/react-dialog.api.md +++ b/packages/react-components/react-dialog/etc/react-dialog.api.md @@ -9,9 +9,9 @@ import { ARIAButtonResultProps } from '@fluentui/react-aria'; import { ARIAButtonType } from '@fluentui/react-aria'; import type { ComponentProps } from '@fluentui/react-utilities'; -import type { ComponentState } from '@fluentui/react-utilities'; import type { ForwardRefComponent } from '@fluentui/react-utilities'; import { JSXElementConstructor } from 'react'; +import type { NextComponentState } from '@fluentui/react-utilities'; import * as React_2 from 'react'; import { ReactElement } from 'react'; import type { Slot } from '@fluentui/react-utilities'; @@ -39,11 +39,11 @@ export type DialogActionsProps = ComponentProps & { // @public (undocumented) export type DialogActionsSlots = { - root: Slot<'div'>; + root: NonNullable>; }; // @public -export type DialogActionsState = ComponentState & Pick, 'position' | 'fluid'>; +export type DialogActionsState = NextComponentState & Pick, 'position' | 'fluid'>; // @public export const DialogBody: ForwardRefComponent; @@ -56,11 +56,11 @@ export type DialogBodyProps = ComponentProps & {}; // @public (undocumented) export type DialogBodySlots = { - root: Slot<'div'>; + root: NonNullable>; }; // @public -export type DialogBodyState = ComponentState; +export type DialogBodyState = NextComponentState; // @public export const DialogContent: ForwardRefComponent; @@ -73,11 +73,11 @@ export type DialogContentProps = ComponentProps; // @public (undocumented) export type DialogContentSlots = { - root: Slot<'div'>; + root: NonNullable>; }; // @public -export type DialogContentState = ComponentState; +export type DialogContentState = NextComponentState; // @public (undocumented) export type DialogOpenChangeData = { @@ -114,7 +114,7 @@ export type DialogProps = ComponentProps> & { export type DialogSlots = {}; // @public (undocumented) -export type DialogState = ComponentState & DialogContextValue & { +export type DialogState = NextComponentState & DialogContextValue & { content: React_2.ReactNode; trigger: React_2.ReactNode; }; @@ -134,11 +134,11 @@ export type DialogSurfaceProps = ComponentProps; // @public (undocumented) export type DialogSurfaceSlots = { backdrop?: Slot<'div'>; - root: Slot<'div'>; + root: NonNullable>; }; // @public -export type DialogSurfaceState = ComponentState; +export type DialogSurfaceState = NextComponentState; // @public export const DialogTitle: ForwardRefComponent; @@ -156,7 +156,7 @@ export type DialogTitleSlots = { }; // @public -export type DialogTitleState = ComponentState; +export type DialogTitleState = NextComponentState; // @public export const DialogTrigger: React_2.FC; diff --git a/packages/react-components/react-dialog/src/components/Dialog/Dialog.types.ts b/packages/react-components/react-dialog/src/components/Dialog/Dialog.types.ts index 6a05e1fe8576df..13eb236e132f6e 100644 --- a/packages/react-components/react-dialog/src/components/Dialog/Dialog.types.ts +++ b/packages/react-components/react-dialog/src/components/Dialog/Dialog.types.ts @@ -1,5 +1,5 @@ import type * as React from 'react'; -import type { ComponentProps, ComponentState } from '@fluentui/react-utilities'; +import type { ComponentProps, NextComponentState } from '@fluentui/react-utilities'; import type { DialogContextValue, DialogSurfaceContextValue } from '../../contexts'; import type { DialogSurfaceElement } from '../DialogSurface/DialogSurface.types'; @@ -96,7 +96,7 @@ export type DialogProps = ComponentProps> & { inertTrapFocus?: boolean; }; -export type DialogState = ComponentState & +export type DialogState = NextComponentState & DialogContextValue & { content: React.ReactNode; trigger: React.ReactNode; diff --git a/packages/react-components/react-dialog/src/components/Dialog/renderDialog.tsx b/packages/react-components/react-dialog/src/components/Dialog/renderDialog.tsx index 320a9b1d2231ab..5429255accfb5e 100644 --- a/packages/react-components/react-dialog/src/components/Dialog/renderDialog.tsx +++ b/packages/react-components/react-dialog/src/components/Dialog/renderDialog.tsx @@ -1,8 +1,4 @@ -/** @jsxRuntime classic */ -/** @jsx createElement */ - -import { createElement } from '@fluentui/react-jsx-runtime'; - +import * as React from 'react'; import { DialogProvider, DialogSurfaceProvider } from '../../contexts'; import type { DialogState, DialogContextValues } from './Dialog.types'; diff --git a/packages/react-components/react-dialog/src/components/Dialog/useDialog.ts b/packages/react-components/react-dialog/src/components/Dialog/useDialog.ts index ffce253ffac893..b526981d41cb99 100644 --- a/packages/react-components/react-dialog/src/components/Dialog/useDialog.ts +++ b/packages/react-components/react-dialog/src/components/Dialog/useDialog.ts @@ -52,9 +52,6 @@ export const useDialog_unstable = (props: DialogProps): DialogState => { }); return { - components: { - backdrop: 'div', - }, inertTrapFocus, open, modalType, diff --git a/packages/react-components/react-dialog/src/components/DialogActions/DialogActions.types.ts b/packages/react-components/react-dialog/src/components/DialogActions/DialogActions.types.ts index b774c394c0e58f..9beaadbf74b3e4 100644 --- a/packages/react-components/react-dialog/src/components/DialogActions/DialogActions.types.ts +++ b/packages/react-components/react-dialog/src/components/DialogActions/DialogActions.types.ts @@ -1,7 +1,7 @@ -import type { ComponentProps, ComponentState, Slot } from '@fluentui/react-utilities'; +import type { ComponentProps, NextComponentState, Slot } from '@fluentui/react-utilities'; export type DialogActionsSlots = { - root: Slot<'div'>; + root: NonNullable>; }; export type DialogActionsPosition = 'start' | 'end'; @@ -26,5 +26,5 @@ export type DialogActionsProps = ComponentProps & { /** * State used in rendering DialogActions */ -export type DialogActionsState = ComponentState & +export type DialogActionsState = NextComponentState & Pick, 'position' | 'fluid'>; diff --git a/packages/react-components/react-dialog/src/components/DialogActions/renderDialogActions.tsx b/packages/react-components/react-dialog/src/components/DialogActions/renderDialogActions.tsx index 570ad41b87a276..28ce505a0c4ce7 100644 --- a/packages/react-components/react-dialog/src/components/DialogActions/renderDialogActions.tsx +++ b/packages/react-components/react-dialog/src/components/DialogActions/renderDialogActions.tsx @@ -1,17 +1,11 @@ /** @jsxRuntime classic */ -/** @jsx createElement */ +/** @jsx createElementNext */ -import { createElement } from '@fluentui/react-jsx-runtime'; +import { createElementNext } from '@fluentui/react-jsx-runtime'; -import { getSlotsNext } from '@fluentui/react-utilities'; -import type { DialogActionsState, DialogActionsSlots } from './DialogActions.types'; +import type { DialogActionsState } from './DialogActions.types'; /** * Render the final JSX of DialogActions */ -export const renderDialogActions_unstable = (state: DialogActionsState) => { - const { slots, slotProps } = getSlotsNext(state); - - // TODO Add additional slots in the appropriate place - return ; -}; +export const renderDialogActions_unstable = (state: DialogActionsState) => ; diff --git a/packages/react-components/react-dialog/src/components/DialogActions/useDialogActions.ts b/packages/react-components/react-dialog/src/components/DialogActions/useDialogActions.ts index e01526d61078da..178c8d5f2ee068 100644 --- a/packages/react-components/react-dialog/src/components/DialogActions/useDialogActions.ts +++ b/packages/react-components/react-dialog/src/components/DialogActions/useDialogActions.ts @@ -1,5 +1,5 @@ import * as React from 'react'; -import { getNativeElementProps } from '@fluentui/react-utilities'; +import { getNativeElementProps, slot } from '@fluentui/react-utilities'; import type { DialogActionsProps, DialogActionsState } from './DialogActions.types'; /** @@ -17,13 +17,13 @@ export const useDialogActions_unstable = ( ): DialogActionsState => { const { position = 'end', fluid = false } = props; return { - components: { - root: 'div', - }, - root: getNativeElementProps('div', { - ref, - ...props, - }), + root: slot( + getNativeElementProps('div', { + ref, + ...props, + }), + { required: true, componentType: 'div' }, + ), position, fluid, }; diff --git a/packages/react-components/react-dialog/src/components/DialogActions/useDialogActionsStyles.styles.ts b/packages/react-components/react-dialog/src/components/DialogActions/useDialogActionsStyles.styles.ts index 7515b34f994506..5ca0c85af1dd9c 100644 --- a/packages/react-components/react-dialog/src/components/DialogActions/useDialogActionsStyles.styles.ts +++ b/packages/react-components/react-dialog/src/components/DialogActions/useDialogActionsStyles.styles.ts @@ -44,14 +44,14 @@ const useStyles = makeStyles({ */ export const useDialogActionsStyles_unstable = (state: DialogActionsState): DialogActionsState => { const styles = useStyles(); - state.root.className = mergeClasses( + state.root.props.className = mergeClasses( dialogActionsClassNames.root, styles.root, state.position === 'start' && styles.gridPositionStart, state.position === 'end' && styles.gridPositionEnd, state.fluid && state.position === 'start' && styles.fluidStart, state.fluid && state.position === 'end' && styles.fluidEnd, - state.root.className, + state.root.props.className, ); return state; }; diff --git a/packages/react-components/react-dialog/src/components/DialogBody/DialogBody.types.ts b/packages/react-components/react-dialog/src/components/DialogBody/DialogBody.types.ts index ab117b650c460b..f526ee3d8f664d 100644 --- a/packages/react-components/react-dialog/src/components/DialogBody/DialogBody.types.ts +++ b/packages/react-components/react-dialog/src/components/DialogBody/DialogBody.types.ts @@ -1,7 +1,7 @@ -import type { ComponentProps, ComponentState, Slot } from '@fluentui/react-utilities'; +import type { ComponentProps, NextComponentState, Slot } from '@fluentui/react-utilities'; export type DialogBodySlots = { - root: Slot<'div'>; + root: NonNullable>; }; /** @@ -12,4 +12,4 @@ export type DialogBodyProps = ComponentProps & {}; /** * State used in rendering DialogBody */ -export type DialogBodyState = ComponentState; +export type DialogBodyState = NextComponentState; diff --git a/packages/react-components/react-dialog/src/components/DialogBody/renderDialogBody.tsx b/packages/react-components/react-dialog/src/components/DialogBody/renderDialogBody.tsx index df955d0229de25..9400b31679a1c1 100644 --- a/packages/react-components/react-dialog/src/components/DialogBody/renderDialogBody.tsx +++ b/packages/react-components/react-dialog/src/components/DialogBody/renderDialogBody.tsx @@ -1,17 +1,11 @@ /** @jsxRuntime classic */ -/** @jsx createElement */ +/** @jsx createElementNext */ -import { createElement } from '@fluentui/react-jsx-runtime'; +import { createElementNext } from '@fluentui/react-jsx-runtime'; -import { getSlotsNext } from '@fluentui/react-utilities'; -import type { DialogBodyState, DialogBodySlots } from './DialogBody.types'; +import type { DialogBodyState } from './DialogBody.types'; /** * Render the final JSX of DialogBody */ -export const renderDialogBody_unstable = (state: DialogBodyState) => { - const { slots, slotProps } = getSlotsNext(state); - - // TODO Add additional slots in the appropriate place - return ; -}; +export const renderDialogBody_unstable = (state: DialogBodyState) => ; diff --git a/packages/react-components/react-dialog/src/components/DialogBody/useDialogBody.ts b/packages/react-components/react-dialog/src/components/DialogBody/useDialogBody.ts index d8f93a866b18a7..50f4611e805cdc 100644 --- a/packages/react-components/react-dialog/src/components/DialogBody/useDialogBody.ts +++ b/packages/react-components/react-dialog/src/components/DialogBody/useDialogBody.ts @@ -1,5 +1,5 @@ import * as React from 'react'; -import { getNativeElementProps } from '@fluentui/react-utilities'; +import { getNativeElementProps, slot } from '@fluentui/react-utilities'; import type { DialogBodyProps, DialogBodyState } from './DialogBody.types'; /** @@ -13,12 +13,12 @@ import type { DialogBodyProps, DialogBodyState } from './DialogBody.types'; */ export const useDialogBody_unstable = (props: DialogBodyProps, ref: React.Ref): DialogBodyState => { return { - components: { - root: 'div', - }, - root: getNativeElementProps(props.as ?? 'div', { - ref, - ...props, - }), + root: slot( + getNativeElementProps(props.as ?? 'div', { + ref, + ...props, + }), + { required: true, componentType: 'div' }, + ), }; }; diff --git a/packages/react-components/react-dialog/src/components/DialogBody/useDialogBodyStyles.styles.ts b/packages/react-components/react-dialog/src/components/DialogBody/useDialogBodyStyles.styles.ts index 519ca92ddcedc8..376ad0d241bb87 100644 --- a/packages/react-components/react-dialog/src/components/DialogBody/useDialogBodyStyles.styles.ts +++ b/packages/react-components/react-dialog/src/components/DialogBody/useDialogBodyStyles.styles.ts @@ -54,7 +54,7 @@ const useStyles = makeStyles({ */ export const useDialogBodyStyles_unstable = (state: DialogBodyState): DialogBodyState => { const styles = useStyles(); - state.root.className = mergeClasses(dialogBodyClassNames.root, styles.root, state.root.className); + state.root.props.className = mergeClasses(dialogBodyClassNames.root, styles.root, state.root.props.className); return state; }; diff --git a/packages/react-components/react-dialog/src/components/DialogContent/DialogContent.types.ts b/packages/react-components/react-dialog/src/components/DialogContent/DialogContent.types.ts index 5b0f1be9cda5d0..5ef3420a50c7e8 100644 --- a/packages/react-components/react-dialog/src/components/DialogContent/DialogContent.types.ts +++ b/packages/react-components/react-dialog/src/components/DialogContent/DialogContent.types.ts @@ -1,7 +1,7 @@ -import type { ComponentProps, ComponentState, Slot } from '@fluentui/react-utilities'; +import type { ComponentProps, NextComponentState, Slot } from '@fluentui/react-utilities'; export type DialogContentSlots = { - root: Slot<'div'>; + root: NonNullable>; }; /** @@ -12,4 +12,4 @@ export type DialogContentProps = ComponentProps; /** * State used in rendering DialogContent */ -export type DialogContentState = ComponentState; +export type DialogContentState = NextComponentState; diff --git a/packages/react-components/react-dialog/src/components/DialogContent/renderDialogContent.tsx b/packages/react-components/react-dialog/src/components/DialogContent/renderDialogContent.tsx index b437f88c95efb1..9394f2d337e46c 100644 --- a/packages/react-components/react-dialog/src/components/DialogContent/renderDialogContent.tsx +++ b/packages/react-components/react-dialog/src/components/DialogContent/renderDialogContent.tsx @@ -1,16 +1,11 @@ /** @jsxRuntime classic */ -/** @jsx createElement */ +/** @jsx createElementNext */ -import { createElement } from '@fluentui/react-jsx-runtime'; +import { createElementNext } from '@fluentui/react-jsx-runtime'; -import { getSlotsNext } from '@fluentui/react-utilities'; -import type { DialogContentState, DialogContentSlots } from './DialogContent.types'; +import type { DialogContentState } from './DialogContent.types'; /** * Render the final JSX of DialogContent */ -export const renderDialogContent_unstable = (state: DialogContentState) => { - const { slots, slotProps } = getSlotsNext(state); - - return ; -}; +export const renderDialogContent_unstable = (state: DialogContentState) => ; diff --git a/packages/react-components/react-dialog/src/components/DialogContent/useDialogContent.ts b/packages/react-components/react-dialog/src/components/DialogContent/useDialogContent.ts index b188629898581a..295b09ea601cb7 100644 --- a/packages/react-components/react-dialog/src/components/DialogContent/useDialogContent.ts +++ b/packages/react-components/react-dialog/src/components/DialogContent/useDialogContent.ts @@ -1,5 +1,5 @@ import * as React from 'react'; -import { getNativeElementProps } from '@fluentui/react-utilities'; +import { getNativeElementProps, slot } from '@fluentui/react-utilities'; import { DialogContentProps, DialogContentState } from './DialogContent.types'; /** @@ -16,12 +16,12 @@ export const useDialogContent_unstable = ( ref: React.Ref, ): DialogContentState => { return { - components: { - root: 'div', - }, - root: getNativeElementProps(props.as ?? 'div', { - ref, - ...props, - }), + root: slot( + getNativeElementProps(props.as ?? 'div', { + ref, + ...props, + }), + { required: true, componentType: 'div' }, + ), }; }; diff --git a/packages/react-components/react-dialog/src/components/DialogContent/useDialogContentStyles.styles.ts b/packages/react-components/react-dialog/src/components/DialogContent/useDialogContentStyles.styles.ts index 9c6c7baf41665e..2eab16e063afc4 100644 --- a/packages/react-components/react-dialog/src/components/DialogContent/useDialogContentStyles.styles.ts +++ b/packages/react-components/react-dialog/src/components/DialogContent/useDialogContentStyles.styles.ts @@ -28,6 +28,6 @@ const useStyles = makeStyles({ */ export const useDialogContentStyles_unstable = (state: DialogContentState): DialogContentState => { const styles = useStyles(); - state.root.className = mergeClasses(dialogContentClassNames.root, styles.root, state.root.className); + state.root.props.className = mergeClasses(dialogContentClassNames.root, styles.root, state.root.props.className); return state; }; diff --git a/packages/react-components/react-dialog/src/components/DialogSurface/DialogSurface.types.ts b/packages/react-components/react-dialog/src/components/DialogSurface/DialogSurface.types.ts index a0d72f37fc6049..ed7c5cbc8be10c 100644 --- a/packages/react-components/react-dialog/src/components/DialogSurface/DialogSurface.types.ts +++ b/packages/react-components/react-dialog/src/components/DialogSurface/DialogSurface.types.ts @@ -1,4 +1,4 @@ -import type { ComponentProps, ComponentState, Slot } from '@fluentui/react-utilities'; +import type { ComponentProps, NextComponentState, Slot } from '@fluentui/react-utilities'; import { DialogSurfaceContextValue } from '../../contexts'; export type DialogSurfaceSlots = { @@ -10,7 +10,7 @@ export type DialogSurfaceSlots = { * */ backdrop?: Slot<'div'>; - root: Slot<'div'>; + root: NonNullable>; }; /** @@ -30,4 +30,4 @@ export type DialogSurfaceContextValues = { /** * State used in rendering DialogSurface */ -export type DialogSurfaceState = ComponentState; +export type DialogSurfaceState = NextComponentState; diff --git a/packages/react-components/react-dialog/src/components/DialogSurface/renderDialogSurface.tsx b/packages/react-components/react-dialog/src/components/DialogSurface/renderDialogSurface.tsx index 76721af33f01a8..a39adec1700102 100644 --- a/packages/react-components/react-dialog/src/components/DialogSurface/renderDialogSurface.tsx +++ b/packages/react-components/react-dialog/src/components/DialogSurface/renderDialogSurface.tsx @@ -1,25 +1,20 @@ /** @jsxRuntime classic */ -/** @jsx createElement */ +/** @jsx createElementNext */ -import { createElement } from '@fluentui/react-jsx-runtime'; +import { createElementNext } from '@fluentui/react-jsx-runtime'; -import { getSlotsNext } from '@fluentui/react-utilities'; -import type { DialogSurfaceState, DialogSurfaceSlots, DialogSurfaceContextValues } from './DialogSurface.types'; +import type { DialogSurfaceState, DialogSurfaceContextValues } from './DialogSurface.types'; import { DialogSurfaceProvider } from '../../contexts'; import { Portal } from '@fluentui/react-portal'; /** * Render the final JSX of DialogSurface */ -export const renderDialogSurface_unstable = (state: DialogSurfaceState, contextValues: DialogSurfaceContextValues) => { - const { slots, slotProps } = getSlotsNext(state); - - return ( - - {slots.backdrop && } - - - - - ); -}; +export const renderDialogSurface_unstable = (state: DialogSurfaceState, contextValues: DialogSurfaceContextValues) => ( + + {state.backdrop && } + + + + +); diff --git a/packages/react-components/react-dialog/src/components/DialogSurface/useDialogSurface.ts b/packages/react-components/react-dialog/src/components/DialogSurface/useDialogSurface.ts index 3cc9418d841403..16e36bf5d15078 100644 --- a/packages/react-components/react-dialog/src/components/DialogSurface/useDialogSurface.ts +++ b/packages/react-components/react-dialog/src/components/DialogSurface/useDialogSurface.ts @@ -1,10 +1,10 @@ import * as React from 'react'; import { getNativeElementProps, - resolveShorthand, useEventCallback, useMergedRefs, isResolvedShorthand, + slot, } from '@fluentui/react-utilities'; import type { DialogSurfaceElement, DialogSurfaceProps, DialogSurfaceState } from './DialogSurface.types'; import { useDialogContext_unstable } from '../../contexts'; @@ -59,27 +59,27 @@ export const useDialogSurface_unstable = ( } }); + const backdropSlot = slot(backdrop, { + componentType: 'div', + required: open && modalType !== 'non-modal', + defaultProps: { 'aria-hidden': 'true' }, + overrides: { onClick: handledBackdropClick }, + }); + return { - components: { - backdrop: 'div', - root: 'div', - }, - backdrop: resolveShorthand(backdrop, { - required: open && modalType !== 'non-modal', - defaultProps: { - 'aria-hidden': 'true', - onClick: handledBackdropClick, - }, - }), - root: getNativeElementProps(as ?? 'div', { - tabIndex: -1, // https://github.com/microsoft/fluentui/issues/25150 - 'aria-modal': modalType !== 'non-modal', - role: modalType === 'alert' ? 'alertdialog' : 'dialog', - 'aria-labelledby': props['aria-label'] ? undefined : dialogTitleID, - ...props, - ...modalAttributes, - onKeyDown: handleKeyDown, - ref: useMergedRefs(ref, dialogRef), - }), + backdrop: backdropSlot, + root: slot( + getNativeElementProps(as ?? 'div', { + tabIndex: -1, // https://github.com/microsoft/fluentui/issues/25150 + 'aria-modal': modalType !== 'non-modal', + role: modalType === 'alert' ? 'alertdialog' : 'dialog', + 'aria-labelledby': props['aria-label'] ? undefined : dialogTitleID, + ...props, + ...modalAttributes, + onKeyDown: handleKeyDown, + ref: useMergedRefs(ref, dialogRef), + }), + { required: true, componentType: 'div' }, + ), }; }; diff --git a/packages/react-components/react-dialog/src/components/DialogSurface/useDialogSurfaceStyles.styles.ts b/packages/react-components/react-dialog/src/components/DialogSurface/useDialogSurfaceStyles.styles.ts index 90c97977b91864..8412d0a46f950b 100644 --- a/packages/react-components/react-dialog/src/components/DialogSurface/useDialogSurfaceStyles.styles.ts +++ b/packages/react-components/react-dialog/src/components/DialogSurface/useDialogSurfaceStyles.styles.ts @@ -69,19 +69,19 @@ export const useDialogSurfaceStyles_unstable = (state: DialogSurfaceState): Dial const styles = useStyles(); const isNestedDialog = useDialogContext_unstable(ctx => ctx.isNestedDialog); - state.root.className = mergeClasses( + state.root.props.className = mergeClasses( dialogSurfaceClassNames.root, styles.root, styles.focusOutline, isNestedDialog && styles.nestedNativeDialogBackdrop, - state.root.className, + state.root.props.className, ); if (state.backdrop) { - state.backdrop.className = mergeClasses( + state.backdrop.props.className = mergeClasses( dialogSurfaceClassNames.backdrop, styles.backdrop, isNestedDialog && styles.nestedDialogBackdrop, - state.backdrop.className, + state.backdrop.props.className, ); } return state; diff --git a/packages/react-components/react-dialog/src/components/DialogTitle/DialogTitle.types.ts b/packages/react-components/react-dialog/src/components/DialogTitle/DialogTitle.types.ts index 7388cb8d1e36ca..ed85d9aaf5ec84 100644 --- a/packages/react-components/react-dialog/src/components/DialogTitle/DialogTitle.types.ts +++ b/packages/react-components/react-dialog/src/components/DialogTitle/DialogTitle.types.ts @@ -1,11 +1,11 @@ -import type { ComponentProps, ComponentState, Slot } from '@fluentui/react-utilities'; +import type { ComponentProps, NextComponentState, Slot } from '@fluentui/react-utilities'; export type DialogTitleSlots = { /** * By default this is a h2, but can be any heading or div, * if `div` is provided do not forget to also provide proper `role="heading"` and `aria-level` attributes */ - root: Slot<'h2', 'h1' | 'h3' | 'h4' | 'h5' | 'h6' | 'div'>; + root: NonNullable>; /** * By default a Dialog with modalType='non-modal' will have a close button action */ @@ -20,4 +20,4 @@ export type DialogTitleProps = ComponentProps; /** * State used in rendering DialogTitle */ -export type DialogTitleState = ComponentState; +export type DialogTitleState = NextComponentState; diff --git a/packages/react-components/react-dialog/src/components/DialogTitle/renderDialogTitle.tsx b/packages/react-components/react-dialog/src/components/DialogTitle/renderDialogTitle.tsx index bc592e6091c3b5..d566474bf1a86a 100644 --- a/packages/react-components/react-dialog/src/components/DialogTitle/renderDialogTitle.tsx +++ b/packages/react-components/react-dialog/src/components/DialogTitle/renderDialogTitle.tsx @@ -1,22 +1,18 @@ /** @jsxRuntime classic */ -/** @jsxFrag Fragment */ -/** @jsx createElement */ +/** @jsx createElementNext */ +/** @jsxFrag React.Fragment */ -import { createElement, Fragment } from '@fluentui/react-jsx-runtime'; +import * as React from 'react'; +import { createElementNext } from '@fluentui/react-jsx-runtime'; -import { getSlotsNext } from '@fluentui/react-utilities'; -import type { DialogTitleState, DialogTitleSlots } from './DialogTitle.types'; +import type { DialogTitleState } from './DialogTitle.types'; /** * Render the final JSX of DialogTitle */ -export const renderDialogTitle_unstable = (state: DialogTitleState) => { - const { slots, slotProps } = getSlotsNext(state); - - return ( - <> - {slotProps.root.children} - {slots.action && } - - ); -}; +export const renderDialogTitle_unstable = (state: DialogTitleState) => ( + <> + + {state.action && } + +); diff --git a/packages/react-components/react-dialog/src/components/DialogTitle/useDialogTitle.tsx b/packages/react-components/react-dialog/src/components/DialogTitle/useDialogTitle.tsx index 5aef3e2cbfb26f..56c99005edc950 100644 --- a/packages/react-components/react-dialog/src/components/DialogTitle/useDialogTitle.tsx +++ b/packages/react-components/react-dialog/src/components/DialogTitle/useDialogTitle.tsx @@ -1,12 +1,14 @@ import * as React from 'react'; -import { getNativeElementProps } from '@fluentui/react-utilities'; +import { getNativeElementProps, slot } from '@fluentui/react-utilities'; import type { DialogTitleProps, DialogTitleState } from './DialogTitle.types'; import { useDialogContext_unstable } from '../../contexts/dialogContext'; import { Dismiss24Regular } from '@fluentui/react-icons'; -import { resolveShorthand } from '@fluentui/react-utilities'; import { DialogTrigger } from '../DialogTrigger/DialogTrigger'; import { useDialogTitleInternalStyles } from './useDialogTitleStyles.styles'; +const defaultRootComponentType = 'h2'; +const defaultActionComponentType = 'div'; + /** * Create the state required to render DialogTitle. * @@ -17,21 +19,21 @@ import { useDialogTitleInternalStyles } from './useDialogTitleStyles.styles'; * @param ref - reference to root HTMLElement of DialogTitle */ export const useDialogTitle_unstable = (props: DialogTitleProps, ref: React.Ref): DialogTitleState => { - const { as, action } = props; + const { as = defaultRootComponentType, action } = props; const modalType = useDialogContext_unstable(ctx => ctx.modalType); const internalStyles = useDialogTitleInternalStyles(); return { - components: { - root: 'h2', - action: 'div', - }, - root: getNativeElementProps(as ?? 'h2', { - ref, - id: useDialogContext_unstable(ctx => ctx.dialogTitleId), - ...props, - }), - action: resolveShorthand(action, { + root: slot( + getNativeElementProps(as, { + ref, + id: useDialogContext_unstable(ctx => ctx.dialogTitleId), + ...props, + }), + { componentType: defaultRootComponentType, required: true }, + ), + action: slot(action, { + componentType: defaultActionComponentType, required: modalType === 'non-modal', defaultProps: { children: ( diff --git a/packages/react-components/react-dialog/src/components/DialogTitle/useDialogTitleStyles.styles.ts b/packages/react-components/react-dialog/src/components/DialogTitle/useDialogTitleStyles.styles.ts index a6c74cef3b6abb..436129632956c9 100644 --- a/packages/react-components/react-dialog/src/components/DialogTitle/useDialogTitleStyles.styles.ts +++ b/packages/react-components/react-dialog/src/components/DialogTitle/useDialogTitleStyles.styles.ts @@ -55,14 +55,18 @@ export const useDialogTitleInternalStyles = makeStyles({ */ export const useDialogTitleStyles_unstable = (state: DialogTitleState): DialogTitleState => { const styles = useStyles(); - state.root.className = mergeClasses( + state.root.props.className = mergeClasses( dialogTitleClassNames.root, styles.root, !state.action && styles.rootWithoutCloseButton, - state.root.className, + state.root.props.className, ); if (state.action) { - state.action.className = mergeClasses(dialogTitleClassNames.action, styles.action, state.action.className); + state.action.props.className = mergeClasses( + dialogTitleClassNames.action, + styles.action, + state.action.props.className, + ); } return state; }; diff --git a/packages/react-components/react-jsx-runtime/etc/react-jsx-runtime.api.md b/packages/react-components/react-jsx-runtime/etc/react-jsx-runtime.api.md index 9ec8f93f3f9b8c..90f1d9334f5094 100644 --- a/packages/react-components/react-jsx-runtime/etc/react-jsx-runtime.api.md +++ b/packages/react-components/react-jsx-runtime/etc/react-jsx-runtime.api.md @@ -10,6 +10,9 @@ import * as React_2 from 'react'; // @public (undocumented) export function createElement

(type: React_2.ElementType

, props?: P | null, ...children: React_2.ReactNode[]): React_2.ReactElement

| null; +// @public (undocumented) +export function createElementNext(type: React_2.ElementType, props?: Props | null, ...children: React_2.ReactNode[]): React_2.ReactElement | null; + export { Fragment } // (No @packageDocumentation comment for this package) diff --git a/packages/react-components/react-jsx-runtime/src/createElement.test.tsx b/packages/react-components/react-jsx-runtime/src/createElement.test.tsx index ee4e3221aa7726..fe3428737697e9 100644 --- a/packages/react-components/react-jsx-runtime/src/createElement.test.tsx +++ b/packages/react-components/react-jsx-runtime/src/createElement.test.tsx @@ -1,8 +1,6 @@ -/* eslint-disable jsdoc/check-tag-names */ /** @jsxRuntime classic */ /** @jsxFrag Fragment */ /** @jsx createElement */ -/* eslint-enable jsdoc/check-tag-names */ import { render } from '@testing-library/react'; import { ComponentProps, ComponentState, Slot, getSlotsNext, resolveShorthand } from '@fluentui/react-utilities'; diff --git a/packages/react-components/react-jsx-runtime/src/createElementNext.test.tsx b/packages/react-components/react-jsx-runtime/src/createElementNext.test.tsx new file mode 100644 index 00000000000000..66e64114454a9b --- /dev/null +++ b/packages/react-components/react-jsx-runtime/src/createElementNext.test.tsx @@ -0,0 +1,160 @@ +/** @jsxRuntime classic */ +/** @jsxFrag Fragment */ +/** @jsx createElementNext */ + +import { render } from '@testing-library/react'; +import { ComponentProps, NextComponentState, Slot, slot } from '@fluentui/react-utilities'; +import { createElementNext } from './createElementNext'; + +describe('createElement next', () => { + describe('general behavior tests', () => { + it('handles a string', () => { + const result = render(

Hello world
); + + expect(result.container.firstChild).toMatchInlineSnapshot(` +
+ Hello world +
+ `); + }); + + it('handles an array', () => { + const result = render( +
+ {Array.from({ length: 3 }, (_, i) => ( +
{i}
+ ))} +
, + ); + + expect(result.container.firstChild).toMatchInlineSnapshot(` +
+
+ 0 +
+
+ 1 +
+
+ 2 +
+
+ `); + }); + + it('handles an array of children', () => { + const result = render( +
+
1
+
2
+
, + ); + + expect(result.container.firstChild).toMatchInlineSnapshot(` +
+
+ 1 +
+
+ 2 +
+
+ `); + }); + }); + + describe('custom behavior tests', () => { + it('keeps children from "defaultProps" in a render callback', () => { + type TestComponentSlots = { slot: NonNullable> }; + type TestComponentState = NextComponentState; + type TestComponentProps = ComponentProps>; + + const TestComponent = (props: TestComponentProps) => { + const state: TestComponentState = { + slot: slot(props.slot, { + required: true, + componentType: 'div', + defaultProps: { children: 'Default Children', id: 'slot' }, + }), + }; + return ; + }; + + const children = jest.fn().mockImplementation((Component, props) => ( +
+ +
+ )); + const result = render(); + + expect(children).toHaveBeenCalledTimes(1); + expect(children).toHaveBeenCalledWith('div', { children: 'Default Children', id: 'slot' }); + + expect(result.container.firstChild).toMatchInlineSnapshot(` +
+
+ Default Children +
+
+ `); + }); + + it('keeps children from a render template in a render callback', () => { + type TestComponentSlots = { outer: NonNullable>; inner: NonNullable> }; + type TestComponentState = NextComponentState; + type TestComponentProps = ComponentProps>; + + const TestComponent = (props: TestComponentProps) => { + const state: TestComponentState = { + inner: slot(props.inner, { required: true, defaultProps: { id: 'inner' }, componentType: 'div' }), + outer: slot(props.outer, { required: true, defaultProps: { id: 'outer' }, componentType: 'div' }), + }; + return ( + + + + ); + }; + + const children = jest.fn().mockImplementation((Component, props) => ( +
+ +
+ )); + const result = render(); + + expect(children).toHaveBeenCalledTimes(1); + expect(children.mock.calls[0][0]).toBe('div'); + expect(children.mock.calls[0][1].id).toBe('outer'); + expect(children.mock.calls[0][1].children).toMatchInlineSnapshot(` + +
+ Inner children +
+
+ `); + + expect(result.container.firstChild).toMatchInlineSnapshot(` +
+
+
+ Inner children +
+
+
+ `); + }); + }); +}); diff --git a/packages/react-components/react-jsx-runtime/src/createElementNext.ts b/packages/react-components/react-jsx-runtime/src/createElementNext.ts new file mode 100644 index 00000000000000..68e4af3929e69d --- /dev/null +++ b/packages/react-components/react-jsx-runtime/src/createElementNext.ts @@ -0,0 +1,32 @@ +import * as React from 'react'; +import { UnknownSlotProps, isSlot } from '@fluentui/react-utilities'; +import { SlotComponent } from '@fluentui/react-utilities/src/compose/types'; + +export function createElementNext( + type: React.ElementType, + props?: Props | null, + ...children: React.ReactNode[] +): React.ReactElement | null { + return isSlot(type) ? createElementFromSlotComponent(type, children) : React.createElement(type, props, ...children); +} + +function createElementFromSlotComponent( + slotComponent: SlotComponent, + overrideChildren: React.ReactNode[], +): React.ReactElement | null { + const { props, renderFunction, componentType } = slotComponent; + const type = componentType as React.ElementType; + + if (renderFunction) { + const children = + overrideChildren.length > 0 ? React.createElement(React.Fragment, {}, ...overrideChildren) : props.children; + + return React.createElement( + React.Fragment, + {}, + renderFunction(type, { ...props, children }), + ) as React.ReactElement; + } + + return React.createElement(type, props, ...overrideChildren); +} diff --git a/packages/react-components/react-jsx-runtime/src/index.ts b/packages/react-components/react-jsx-runtime/src/index.ts index 0276e539054a37..c6a5b85bc3fb21 100644 --- a/packages/react-components/react-jsx-runtime/src/index.ts +++ b/packages/react-components/react-jsx-runtime/src/index.ts @@ -1,2 +1,3 @@ export { createElement } from './createElement'; +export { createElementNext } from './createElementNext'; export { Fragment } from 'react'; 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 a03ffd720af387..15c68b9bdba4c0 100644 --- a/packages/react-components/react-utilities/etc/react-utilities.api.md +++ b/packages/react-components/react-utilities/etc/react-utilities.api.md @@ -101,6 +101,9 @@ export function isMouseEvent(event: TouchOrMouseEvent): event is MouseEvent | Re // @public export function isResolvedShorthand>(shorthand?: Shorthand): shorthand is ExtractSlotProps; +// @public (undocumented) +export function isSlot

(element: React_2.ElementType

): element is SlotComponent

; + // @public export function isTouchEvent(event: TouchOrMouseEvent): event is TouchEvent | React_2.TouchEvent; @@ -110,6 +113,11 @@ export function mergeCallbacks(callback1: ((...args: Arg // @public (undocumented) export type NativeTouchOrMouseEvent = MouseEvent | TouchEvent; +// @public +export type NextComponentState = { + [Key in keyof Slots]: SlotComponent>; +}; + // @public export function omit, Exclusions extends (keyof TObj)[]>(obj: TObj, exclusions: Exclusions): Omit; @@ -149,6 +157,16 @@ export type Slot>; }[AlternateAs] | null : 'Error: First parameter to Slot must not be not a union of types. See documentation of Slot type.'; +// @public (undocumented) +export function slot(shorthand: Props | SlotShorthandValue | undefined, options: SlotOptions & { + required: true; +}): SlotComponent; + +// @public (undocumented) +export function slot(shorthand: Props | SlotShorthandValue | null | undefined, options: SlotOptions & { + required?: boolean; +}): SlotComponent | undefined; + // @internal export const SLOT_RENDER_FUNCTION_SYMBOL: unique symbol; diff --git a/packages/react-components/react-utilities/src/compose/constants.ts b/packages/react-components/react-utilities/src/compose/constants.ts index f5c1593f9203ae..dc167d8d632fec 100644 --- a/packages/react-components/react-utilities/src/compose/constants.ts +++ b/packages/react-components/react-utilities/src/compose/constants.ts @@ -3,3 +3,9 @@ * Internal reference for the render function */ export const SLOT_RENDER_FUNCTION_SYMBOL = Symbol('fui.slotRenderFunction'); + +/** + * @internal + * Internal value that indicates a given component is a slot component + */ +export const SLOT_COMPONENT_SYMBOL = Symbol('fui.slotComponent'); diff --git a/packages/react-components/react-utilities/src/compose/index.ts b/packages/react-components/react-utilities/src/compose/index.ts index fec1ddf85added..fc39aa1a92ea96 100644 --- a/packages/react-components/react-utilities/src/compose/index.ts +++ b/packages/react-components/react-utilities/src/compose/index.ts @@ -4,3 +4,4 @@ export * from './types'; export * from './isResolvedShorthand'; export * from './constants'; export * from './getSlotsNext'; +export * from './slot'; diff --git a/packages/react-components/react-utilities/src/compose/slot.ts b/packages/react-components/react-utilities/src/compose/slot.ts new file mode 100644 index 00000000000000..eab8d9fbafaab5 --- /dev/null +++ b/packages/react-components/react-utilities/src/compose/slot.ts @@ -0,0 +1,61 @@ +import { isValidElement } from 'react'; +import type { AsIntrinsicElement, SlotComponent, SlotShorthandValue, UnknownSlotProps } from './types'; +import { SLOT_COMPONENT_SYMBOL } from './constants'; +import * as React from 'react'; + +export type SlotOptions = { + defaultProps?: Partial; + overrides?: Partial; + componentType: + | React.ComponentType + | (Props extends AsIntrinsicElement ? As : keyof JSX.IntrinsicElements); +}; + +export function slot( + shorthand: Props | SlotShorthandValue | undefined, + options: SlotOptions & { required: true }, +): SlotComponent; +export function slot( + shorthand: Props | SlotShorthandValue | null | undefined, + options: SlotOptions & { required?: boolean }, +): SlotComponent | undefined; +export function slot( + shorthand: Props | SlotShorthandValue | null | undefined, + options: SlotOptions & { required?: boolean }, +): SlotComponent | undefined { + const { required = false, defaultProps, overrides, componentType } = options; + + if (shorthand === null || (shorthand === undefined && !required)) { + return undefined; + } + + const props = { ...defaultProps }; + let renderFunction: Function | undefined; + + if ( + typeof shorthand === 'string' || + typeof shorthand === 'number' || + Array.isArray(shorthand) || + // eslint-disable-next-line @typescript-eslint/no-explicit-any + isValidElement(shorthand) + ) { + props.children = shorthand; + } else if (typeof shorthand === 'object') { + Object.assign(props, shorthand); + if (typeof shorthand.children === 'function') { + renderFunction = shorthand.children; + props.children = defaultProps?.children; + } + } + + return { + componentType, + renderFunction, + $$typeof: SLOT_COMPONENT_SYMBOL, + props: Object.assign(props, overrides), + } as SlotComponent; +} + +export function isSlot

(element: React.ElementType

): element is SlotComponent

{ + return Boolean((element as React.ExoticComponent).$$typeof === SLOT_COMPONENT_SYMBOL); +} diff --git a/packages/react-components/react-utilities/src/compose/types.ts b/packages/react-components/react-utilities/src/compose/types.ts index 8169dad5cc4e95..e874d704dcc032 100644 --- a/packages/react-components/react-utilities/src/compose/types.ts +++ b/packages/react-components/react-utilities/src/compose/types.ts @@ -1,4 +1,5 @@ import * as React from 'react'; +import { SLOT_COMPONENT_SYMBOL } from './constants'; export type SlotRenderFunction = ( Component: React.ElementType, @@ -199,6 +200,13 @@ export type ComponentState = { >; }; +/** + * Defines the State object of a component given its slots. + */ +export type NextComponentState = { + [Key in keyof Slots]: SlotComponent>; +}; + /** * This is part of a hack to infer the element type from a native element *props* type. * The only place the original element is found in a native props type (at least that's workable @@ -233,3 +241,14 @@ export type ForwardRefComponent = ObscureEventName extends keyof Props export type SlotClassNames = { [SlotName in keyof Slots]-?: string; }; + +export type SlotComponent = React.ExoticComponent< + React.PropsWithChildren<{}> +> & { + readonly props: Props; + readonly renderFunction?: SlotRenderFunction; + readonly componentType: + | React.ComponentType + | (Props extends AsIntrinsicElement ? As : keyof JSX.IntrinsicElements); + readonly $$typeof: typeof SLOT_COMPONENT_SYMBOL; +}; diff --git a/packages/react-components/react-utilities/src/index.ts b/packages/react-components/react-utilities/src/index.ts index c2d0023253e0b3..d992c6a6ac3e14 100644 --- a/packages/react-components/react-utilities/src/index.ts +++ b/packages/react-components/react-utilities/src/index.ts @@ -1,4 +1,6 @@ export { + slot, + isSlot, getSlots, getSlotsNext, resolveShorthand, @@ -8,6 +10,7 @@ export { export type { ExtractSlotProps, ComponentProps, + NextComponentState, ComponentState, ForwardRefComponent, ResolveShorthandFunction,