diff --git a/change/@fluentui-react-components-05ea9e2c-59c4-4b95-8d7a-e6fc6ee1bb5c.json b/change/@fluentui-react-components-05ea9e2c-59c4-4b95-8d7a-e6fc6ee1bb5c.json new file mode 100644 index 0000000000000..b02aef9624bb0 --- /dev/null +++ b/change/@fluentui-react-components-05ea9e2c-59c4-4b95-8d7a-e6fc6ee1bb5c.json @@ -0,0 +1,7 @@ +{ + "type": "minor", + "comment": "feat: make useTransitionPresence hook", + "packageName": "@fluentui/react-components", + "email": "marcosvmmoura@gmail.com", + "dependentChangeType": "patch" +} diff --git a/change/@fluentui-react-dialog-58d6263b-fb53-461c-8144-1c5ac2ff9003.json b/change/@fluentui-react-dialog-58d6263b-fb53-461c-8144-1c5ac2ff9003.json new file mode 100644 index 0000000000000..386fa158df8bd --- /dev/null +++ b/change/@fluentui-react-dialog-58d6263b-fb53-461c-8144-1c5ac2ff9003.json @@ -0,0 +1,7 @@ +{ + "type": "minor", + "comment": "feat: add motion to dialog", + "packageName": "@fluentui/react-dialog", + "email": "marcosvmmoura@gmail.com", + "dependentChangeType": "patch" +} diff --git a/change/@fluentui-react-drawer-906d4815-2dd6-43b5-af6e-4ef4571ed915.json b/change/@fluentui-react-drawer-906d4815-2dd6-43b5-af6e-4ef4571ed915.json new file mode 100644 index 0000000000000..328e8990f91a1 --- /dev/null +++ b/change/@fluentui-react-drawer-906d4815-2dd6-43b5-af6e-4ef4571ed915.json @@ -0,0 +1,7 @@ +{ + "type": "prerelease", + "comment": "feat: add transition on open/close", + "packageName": "@fluentui/react-drawer", + "email": "marcosvmmoura@gmail.com", + "dependentChangeType": "patch" +} diff --git a/change/@fluentui-react-utilities-02937573-2ff4-49a6-8fec-9dbc38b1644a.json b/change/@fluentui-react-utilities-02937573-2ff4-49a6-8fec-9dbc38b1644a.json new file mode 100644 index 0000000000000..e6efe516a9c2e --- /dev/null +++ b/change/@fluentui-react-utilities-02937573-2ff4-49a6-8fec-9dbc38b1644a.json @@ -0,0 +1,7 @@ +{ + "type": "minor", + "comment": "feat: make useTransitionPresence hook", + "packageName": "@fluentui/react-utilities", + "email": "marcosvmmoura@gmail.com", + "dependentChangeType": "patch" +} diff --git a/packages/react-components/react-components/etc/react-components.api.md b/packages/react-components/react-components/etc/react-components.api.md index 9b2d5de0b5859..7ed103632d2ed 100644 --- a/packages/react-components/react-components/etc/react-components.api.md +++ b/packages/react-components/react-components/etc/react-components.api.md @@ -1109,6 +1109,9 @@ import { useMenuTriggerContext_unstable } from '@fluentui/react-menu'; import { useMergedRefs } from '@fluentui/react-utilities'; import { useModalAttributes } from '@fluentui/react-tabster'; import { UseModalAttributesOptions } from '@fluentui/react-tabster'; +import { useMotionPresence } from '@fluentui/react-utilities'; +import { UseMotionPresenceEvents } from '@fluentui/react-utilities'; +import { UseMotionPresenceState } from '@fluentui/react-utilities'; import { useObservedElement } from '@fluentui/react-tabster'; import { useOption_unstable } from '@fluentui/react-combobox'; import { useOptionGroup_unstable } from '@fluentui/react-combobox'; @@ -3459,6 +3462,12 @@ export { useModalAttributes } export { UseModalAttributesOptions } +export { useMotionPresence } + +export { UseMotionPresenceEvents } + +export { UseMotionPresenceState } + export { useObservedElement } export { useOption_unstable } 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 c12aa430a27f7..8a33fbacf13d8 100644 --- a/packages/react-components/react-dialog/etc/react-dialog.api.md +++ b/packages/react-components/react-dialog/etc/react-dialog.api.md @@ -11,8 +11,11 @@ import { ARIAButtonType } from '@fluentui/react-aria'; import type { ComponentProps } from '@fluentui/react-utilities'; import type { ComponentState } from '@fluentui/react-utilities'; import { ContextSelector } from '@fluentui/react-context-selector'; +import type { ExtractSlotProps } from '@fluentui/react-utilities'; import type { ForwardRefComponent } from '@fluentui/react-utilities'; import { JSXElementConstructor } from 'react'; +import { MotionShorthand } from '@fluentui/react-motion-preview'; +import { MotionState } from '@fluentui/react-motion-preview'; import type { PortalProps } from '@fluentui/react-portal'; import { Provider } from 'react'; import * as React_2 from 'react'; @@ -84,7 +87,8 @@ export type DialogContentState = ComponentState; // @public (undocumented) export type DialogContextValue = { - open: boolean; + open: MotionShorthand; + motion: MotionState; inertTrapFocus: boolean; dialogTitleId?: string; isNestedDialog: boolean; @@ -117,7 +121,7 @@ export type DialogOpenChangeEventHandler = (event: DialogOpenChangeEvent, data: // @public (undocumented) export type DialogProps = ComponentProps> & { modalType?: DialogModalType; - open?: boolean; + open?: MotionShorthand; defaultOpen?: boolean; onOpenChange?: DialogOpenChangeEventHandler; children: [JSX.Element, JSX.Element] | JSX.Element; @@ -156,12 +160,15 @@ export const DialogSurfaceProvider: Provider; // @public (undocumented) export type DialogSurfaceSlots = { - backdrop?: Slot<'div'>; + backdrop?: Slot; root: Slot<'div'>; }; // @public -export type DialogSurfaceState = ComponentState & Pick; +export type DialogSurfaceState = ComponentState & { + motion: MotionState; + backdropMotion: MotionState; +} & Pick; // @public export const DialogTitle: ForwardRefComponent; diff --git a/packages/react-components/react-dialog/package.json b/packages/react-components/react-dialog/package.json index eb16b9dd3b7f3..6f15cbf7045a2 100644 --- a/packages/react-components/react-dialog/package.json +++ b/packages/react-components/react-dialog/package.json @@ -36,16 +36,17 @@ "@fluentui/scripts-tasks": "*" }, "dependencies": { - "@fluentui/react-utilities": "^9.13.5", - "@fluentui/react-jsx-runtime": "^9.0.12", "@fluentui/keyboard-keys": "^9.0.6", - "@fluentui/react-context-selector": "^9.1.36", - "@fluentui/react-shared-contexts": "^9.9.2", "@fluentui/react-aria": "^9.3.38", + "@fluentui/react-context-selector": "^9.1.36", "@fluentui/react-icons": "^2.0.217", + "@fluentui/react-jsx-runtime": "^9.0.3", + "@fluentui/react-motion-preview": "0.2.10", + "@fluentui/react-portal": "^9.3.9", + "@fluentui/react-shared-contexts": "^9.9.2", "@fluentui/react-tabster": "^9.13.2", "@fluentui/react-theme": "^9.1.14", - "@fluentui/react-portal": "^9.3.19", + "@fluentui/react-utilities": "^9.13.5", "@griffel/react": "^1.5.14", "@swc/helpers": "^0.5.1" }, 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 e91b0e3d1ba22..e282b67f95c27 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 @@ -2,6 +2,7 @@ import type * as React from 'react'; import type { ComponentProps, ComponentState } from '@fluentui/react-utilities'; import type { DialogContextValue, DialogSurfaceContextValue } from '../../contexts'; import type { DialogSurfaceElement } from '../DialogSurface/DialogSurface.types'; +import type { MotionShorthand } from '@fluentui/react-motion-preview'; export type DialogSlots = {}; @@ -68,7 +69,7 @@ export type DialogProps = ComponentProps> & { * Controls the open state of the dialog * @default false */ - open?: boolean; + open?: MotionShorthand; /** * Default value for the uncontrolled open state of the dialog. * @default false 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 b08036b9ca834..0e1ba080f7a19 100644 --- a/packages/react-components/react-dialog/src/components/Dialog/useDialog.ts +++ b/packages/react-components/react-dialog/src/components/Dialog/useDialog.ts @@ -6,6 +6,7 @@ import { DialogContext } from '../../contexts'; import type { DialogOpenChangeData, DialogProps, DialogState } from './Dialog.types'; import { useModalAttributes } from '@fluentui/react-tabster'; +import { useMotion } from '@fluentui/react-motion-preview'; /** * Create the state required to render Dialog. @@ -26,6 +27,8 @@ export const useDialog_unstable = (props: DialogProps): DialogState => { initialState: false, }); + const motion = useMotion(open); + const requestOpenChange = useEventCallback((data: DialogOpenChangeData) => { onOpenChange?.(data.event, data); @@ -36,7 +39,7 @@ export const useDialog_unstable = (props: DialogProps): DialogState => { } }); - const focusRef = useFocusFirstElement(open, modalType); + const focusRef = useFocusFirstElement(motion.canRender, modalType); const disableBodyScroll = useDisableBodyScroll(); const isBodyScrollLocked = Boolean(open && modalType !== 'non-modal'); @@ -52,13 +55,12 @@ export const useDialog_unstable = (props: DialogProps): DialogState => { }); return { - components: { - backdrop: 'div', - }, - inertTrapFocus, open, + motion, + components: {}, + inertTrapFocus, modalType, - content: open ? content : null, + content: motion.canRender ? content : null, trigger, requestOpenChange, dialogTitleId: useId('dialog-title-'), diff --git a/packages/react-components/react-dialog/src/components/Dialog/useDialogContextValues.ts b/packages/react-components/react-dialog/src/components/Dialog/useDialogContextValues.ts index 7670d1f8eb8ed..9ce005e1ed5ba 100644 --- a/packages/react-components/react-dialog/src/components/Dialog/useDialogContextValues.ts +++ b/packages/react-components/react-dialog/src/components/Dialog/useDialogContextValues.ts @@ -3,14 +3,15 @@ import type { DialogContextValues, DialogState } from './Dialog.types'; export function useDialogContextValues_unstable(state: DialogState): DialogContextValues { const { - modalType, - open, dialogRef, dialogTitleId, - isNestedDialog, inertTrapFocus, - requestOpenChange, + isNestedDialog, modalAttributes, + modalType, + motion, + open, + requestOpenChange, triggerAttributes, } = state; @@ -19,15 +20,16 @@ export function useDialogContextValues_unstable(state: DialogState): DialogConte * there is no sense to memoize it */ const dialog: DialogContextValue = { - open, - modalType, dialogRef, dialogTitleId, - isNestedDialog, inertTrapFocus, + isNestedDialog, modalAttributes, - triggerAttributes, + modalType, + motion, + open, requestOpenChange, + triggerAttributes, }; const dialogSurface: DialogSurfaceContextValue = false; 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 81026a777eeb2..594c3d217b158 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,6 +1,11 @@ -import type { ComponentProps, ComponentState, Slot } from '@fluentui/react-utilities'; +import type { ComponentProps, ComponentState, ExtractSlotProps, Slot } from '@fluentui/react-utilities'; import type { PortalProps } from '@fluentui/react-portal'; import { DialogSurfaceContextValue } from '../../contexts'; +import { MotionShorthand, MotionState } from '@fluentui/react-motion-preview'; + +export type DialogBackdropProps = ExtractSlotProps> & { + motion?: MotionShorthand; +}; export type DialogSurfaceSlots = { /** @@ -10,7 +15,7 @@ export type DialogSurfaceSlots = { * The backdrop should have `aria-hidden="true"`. * */ - backdrop?: Slot<'div'>; + backdrop?: Slot; root: Slot<'div'>; }; @@ -31,4 +36,7 @@ export type DialogSurfaceContextValues = { /** * State used in rendering DialogSurface */ -export type DialogSurfaceState = ComponentState & Pick; +export type DialogSurfaceState = ComponentState & + Pick & { + motion: MotionState; + }; 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 9cd9f3b35863e..31c846be12819 100644 --- a/packages/react-components/react-dialog/src/components/DialogSurface/useDialogSurface.ts +++ b/packages/react-components/react-dialog/src/components/DialogSurface/useDialogSurface.ts @@ -6,7 +6,7 @@ import { isResolvedShorthand, slot, } from '@fluentui/react-utilities'; -import type { DialogSurfaceElement, DialogSurfaceProps, DialogSurfaceState } from './DialogSurface.types'; +import { DialogSurfaceElement, DialogSurfaceProps, DialogSurfaceState } from './DialogSurface.types'; import { useDialogContext_unstable } from '../../contexts'; import { Escape } from '@fluentui/keyboard-keys'; @@ -26,7 +26,7 @@ export const useDialogSurface_unstable = ( const modalType = useDialogContext_unstable(ctx => ctx.modalType); const modalAttributes = useDialogContext_unstable(ctx => ctx.modalAttributes); const dialogRef = useDialogContext_unstable(ctx => ctx.dialogRef); - const open = useDialogContext_unstable(ctx => ctx.open); + const motion = useDialogContext_unstable(ctx => ctx.motion); const requestOpenChange = useDialogContext_unstable(ctx => ctx.requestOpenChange); const dialogTitleID = useDialogContext_unstable(ctx => ctx.dialogTitleId); @@ -58,16 +58,20 @@ export const useDialogSurface_unstable = ( } }); - const backdrop = slot.optional(props.backdrop, { - renderByDefault: open && modalType !== 'non-modal', - defaultProps: { - 'aria-hidden': 'true', - }, - elementType: 'div', - }); + const backdrop = + motion.canRender && modalType !== 'non-modal' + ? slot.optional(props.backdrop, { + defaultProps: { + 'aria-hidden': 'true', + }, + elementType: 'div', + }) + : undefined; + if (backdrop) { backdrop.onClick = handledBackdropClick; } + return { components: { backdrop: 'div', root: 'div' }, backdrop, @@ -81,9 +85,11 @@ export const useDialogSurface_unstable = ( ...props, ...modalAttributes, onKeyDown: handleKeyDown, - ref: useMergedRefs(ref, dialogRef), + ref: useMergedRefs(ref, dialogRef, motion.ref), }), { elementType: 'div' }, ), + + motion, }; }; 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 699e7badbedc4..49c1aaa6f65dc 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 @@ -2,6 +2,7 @@ import { GriffelStyle, makeResetStyles, makeStyles, mergeClasses, shorthands } f import type { SlotClassNames } from '@fluentui/react-utilities'; import { tokens } from '@fluentui/react-theme'; import { createFocusOutlineStyle } from '@fluentui/react-tabster'; + import { MEDIA_QUERY_BREAKPOINT_SELECTOR, SURFACE_BORDER_WIDTH, @@ -60,13 +61,37 @@ const useRootResetStyles = makeResetStyles({ }, }); -const useStyles = makeStyles({ +const useRootStyles = makeStyles({ nestedDialogBackdrop: nestedDialogBackdropStyles, nestedNativeDialogBackdrop: { '&::backdrop': nestedDialogBackdropStyles, }, }); +const useRootMotionStyles = makeStyles({ + root: { + opacity: 0, + boxShadow: '0px 0px 0px 0px rgba(0, 0, 0, 0.1)', + transform: 'scale(0.85) translateZ(0)', + transitionDuration: tokens.durationGentle, + transitionProperty: 'opacity, transform, box-shadow', + }, + + visible: { + boxShadow: tokens.shadow64, + opacity: 1, + transform: 'scale(1) translateZ(0)', + }, + + entering: { + transitionTimingFunction: tokens.curveDecelerateMid, + }, + + exiting: { + transitionTimingFunction: tokens.curveAccelerateMin, + }, +}); + /** * Styles for the backdrop slot */ @@ -76,19 +101,38 @@ const useBackdropResetStyles = makeResetStyles({ position: 'fixed', }); +const useBackdropMotionStyles = makeStyles({ + backdrop: { + transitionDuration: tokens.durationGentle, + transitionTimingFunction: tokens.curveLinear, + transitionProperty: 'opacity', + willChange: 'opacity', + opacity: 0, + }, + visible: { + opacity: 1, + }, +}); + /** * Apply styling to the DialogSurface slots based on the state */ export const useDialogSurfaceStyles_unstable = (state: DialogSurfaceState): DialogSurfaceState => { const surfaceResetStyles = useRootResetStyles(); - const styles = useStyles(); + const styles = useRootStyles(); + const motionStyles = useRootMotionStyles(); const backdropResetStyles = useBackdropResetStyles(); + const backdropMotionStyles = useBackdropMotionStyles(); const isNestedDialog = useDialogContext_unstable(ctx => ctx.isNestedDialog); state.root.className = mergeClasses( dialogSurfaceClassNames.root, surfaceResetStyles, isNestedDialog && styles.nestedNativeDialogBackdrop, + motionStyles.root, + state.motion.active && motionStyles.visible, + state.motion.type === 'entering' && motionStyles.entering, + state.motion.type === 'exiting' && motionStyles.exiting, state.root.className, ); @@ -97,6 +141,8 @@ export const useDialogSurfaceStyles_unstable = (state: DialogSurfaceState): Dial dialogSurfaceClassNames.backdrop, backdropResetStyles, isNestedDialog && styles.nestedDialogBackdrop, + backdropMotionStyles.backdrop, + state.motion.active && backdropMotionStyles.visible, state.backdrop.className, ); } diff --git a/packages/react-components/react-dialog/src/contexts/dialogContext.ts b/packages/react-components/react-dialog/src/contexts/dialogContext.ts index c186f2f0b5ddf..594638cb5a056 100644 --- a/packages/react-components/react-dialog/src/contexts/dialogContext.ts +++ b/packages/react-components/react-dialog/src/contexts/dialogContext.ts @@ -4,9 +4,11 @@ import { DialogSurfaceElement } from '../DialogSurface'; import type { Context } from '@fluentui/react-context-selector'; import type { DialogModalType, DialogOpenChangeData } from '../Dialog'; import { useModalAttributes } from '@fluentui/react-tabster'; +import { getDefaultMotionState, MotionShorthand, MotionState } from '@fluentui/react-motion-preview'; export type DialogContextValue = { - open: boolean; + open: MotionShorthand; + motion: MotionState; inertTrapFocus: boolean; dialogTitleId?: string; isNestedDialog: boolean; @@ -20,6 +22,7 @@ export type DialogContextValue = { const defaultContextValue: DialogContextValue = { open: false, + motion: getDefaultMotionState(), inertTrapFocus: false, modalType: 'modal', isNestedDialog: false, diff --git a/packages/react-components/react-dialog/src/testing/mockUseDialogContext.ts b/packages/react-components/react-dialog/src/testing/mockUseDialogContext.ts index 9d66e16e935b0..9508746218951 100644 --- a/packages/react-components/react-dialog/src/testing/mockUseDialogContext.ts +++ b/packages/react-components/react-dialog/src/testing/mockUseDialogContext.ts @@ -8,6 +8,12 @@ import type { DialogContextValue } from '../contexts/dialogContext'; */ export const mockUseDialogContext = (options: Partial = {}) => { const mockContext: DialogContextValue = { + motion: { + ref: { current: null }, + type: 'unmounted', + isActive: () => false, + isVisible: () => false, + }, open: false, modalType: 'modal', inertTrapFocus: false, diff --git a/packages/react-components/react-drawer/stories/Drawer/DrawerDisabledTransition.stories.tsx b/packages/react-components/react-drawer/stories/Drawer/DrawerDisabledTransition.stories.tsx new file mode 100644 index 0000000000000..821a1a9095c3b --- /dev/null +++ b/packages/react-components/react-drawer/stories/Drawer/DrawerDisabledTransition.stories.tsx @@ -0,0 +1,50 @@ +import * as React from 'react'; +import { Drawer, DrawerBody, DrawerHeader, DrawerHeaderTitle } from '@fluentui/react-drawer'; +import { Button, makeStyles } from '@fluentui/react-components'; +import { Dismiss24Regular } from '@fluentui/react-icons'; + +const useStyles = makeStyles({ + disabledTransition: { + transitionDuration: '0ms', + }, +}); + +export const DisabledTransition = () => { + const styles = useStyles(); + + const [isOpen, setIsOpen] = React.useState(false); + + return ( +
+ setIsOpen(open)} + > + + } + onClick={() => setIsOpen(false)} + /> + } + > + Drawer with no transition + + + + +

Drawer content

+
+
+ + +
+ ); +}; diff --git a/packages/react-components/react-motion-preview/etc/react-motion-preview.api.md b/packages/react-components/react-motion-preview/etc/react-motion-preview.api.md index 0361130acce4c..249e0fe3ee652 100644 --- a/packages/react-components/react-motion-preview/etc/react-motion-preview.api.md +++ b/packages/react-components/react-motion-preview/etc/react-motion-preview.api.md @@ -5,6 +5,8 @@ ```ts import * as React_2 from 'react'; +import { SlotShorthandValue } from '@fluentui/react-utilities'; +import { UnknownSlotProps } from '@fluentui/react-utilities'; // @public export function getDefaultMotionState(): MotionState; @@ -31,9 +33,21 @@ export type MotionState = { // @public (undocumented) export type MotionType = 'entering' | 'entered' | 'idle' | 'exiting' | 'exited' | 'unmounted'; +// @public (undocumented) +export interface UnknownSlotPropsWithMotion extends UnknownSlotProps { + // (undocumented) + motion?: MotionShorthand; +} + +// @internal +export function useIsMotion(shorthand: MotionShorthand): shorthand is MotionState; + // @public export function useMotion(shorthand: MotionShorthand, options?: MotionOptions): MotionState; +// @internal +export function useMotionFromSlot(props: Props | SlotShorthandValue | undefined | null, shorthand: MotionShorthand, options?: MotionOptions): [Props, MotionState]; + // (No @packageDocumentation comment for this package) ``` diff --git a/packages/react-components/react-motion-preview/src/hooks/index.ts b/packages/react-components/react-motion-preview/src/hooks/index.ts index 4fb9f2d6d5f17..3883d10f49a97 100644 --- a/packages/react-components/react-motion-preview/src/hooks/index.ts +++ b/packages/react-components/react-motion-preview/src/hooks/index.ts @@ -1 +1,2 @@ export * from './useMotion'; +export * from './useMotionFromSlot'; diff --git a/packages/react-components/react-motion-preview/src/hooks/useMotionFromSlot.ts b/packages/react-components/react-motion-preview/src/hooks/useMotionFromSlot.ts new file mode 100644 index 0000000000000..40ee7fd2f98ca --- /dev/null +++ b/packages/react-components/react-motion-preview/src/hooks/useMotionFromSlot.ts @@ -0,0 +1,26 @@ +import { UnknownSlotProps, slot, SlotShorthandValue } from '@fluentui/react-utilities'; +import { MotionShorthand, MotionState, useMotion, MotionOptions } from './useMotion'; + +export interface UnknownSlotPropsWithMotion extends UnknownSlotProps { + motion?: MotionShorthand; +} + +/** + * @internal + * + * Hook to manage the presence of an element in the DOM based on its CSS transition/animation state. + * + * @param props - Motion props to manage the presence of an element in the DOM + * @param options - Motion options to configure the hook + */ +export function useMotionFromSlot( + props: Props | SlotShorthandValue | undefined | null, + shorthand: MotionShorthand, + options?: MotionOptions, +): [Props, MotionState] { + const shorthandProps = slot.resolveShorthand(props); + const { motion: motionProp, ...slotProps } = (shorthandProps ?? {}) as UnknownSlotPropsWithMotion; + const motion = useMotion(motionProp ? motionProp : shorthand, options); + + return [slotProps as Props, motion]; +} diff --git a/packages/react-components/react-motion-preview/src/index.ts b/packages/react-components/react-motion-preview/src/index.ts index dc8bd77378016..cb2443d707117 100644 --- a/packages/react-components/react-motion-preview/src/index.ts +++ b/packages/react-components/react-motion-preview/src/index.ts @@ -1,2 +1,9 @@ -export { getDefaultMotionState, useMotion } from './hooks'; -export type { MotionShorthand, MotionShorthandValue, MotionState, MotionType, MotionOptions } from './hooks'; +export { getDefaultMotionState, useIsMotion, useMotion, useMotionFromSlot } from './hooks'; +export type { + MotionOptions, + MotionShorthand, + MotionShorthandValue, + MotionState, + MotionType, + UnknownSlotPropsWithMotion, +} from './hooks'; diff --git a/packages/react-components/react-motion-preview/stories/useMotion/UseMotionBestPractices.md b/packages/react-components/react-motion-preview/stories/useMotion/UseMotionBestPractices.md new file mode 100644 index 0000000000000..08ff8ddeeb5f8 --- /dev/null +++ b/packages/react-components/react-motion-preview/stories/useMotion/UseMotionBestPractices.md @@ -0,0 +1,5 @@ +## Best practices + +### Do + +### Don't diff --git a/packages/react-components/react-motion-preview/stories/useMotion/UseMotionDefault.stories.tsx b/packages/react-components/react-motion-preview/stories/useMotion/UseMotionDefault.stories.tsx new file mode 100644 index 0000000000000..b6628f71ebd43 --- /dev/null +++ b/packages/react-components/react-motion-preview/stories/useMotion/UseMotionDefault.stories.tsx @@ -0,0 +1,60 @@ +import * as React from 'react'; + +import { useMotion } from '@fluentui/react-motion-preview'; +import { Button, makeStyles, mergeClasses, shorthands, tokens } from '@fluentui/react-components'; + +const useStyles = makeStyles({ + root: { + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + justifyContent: 'center', + rowGap: '24px', + }, + + rectangle: { + ...shorthands.borderRadius('8px'), + + width: '200px', + height: '150px', + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + justifyContent: 'center', + backgroundColor: tokens.colorBrandBackground2, + opacity: 0, + transform: 'translate3D(0, 0, 0) scale(0.25)', + transitionDuration: `${tokens.durationNormal}, ${tokens.durationNormal}, ${tokens.durationUltraSlow}`, + transitionDelay: `${tokens.durationFast}, 0, ${tokens.durationSlow}`, + transitionProperty: 'opacity, transform, background-color', + willChange: 'opacity, transform, background-color', + color: '#fff', + }, + + visible: { + opacity: 1, + transform: 'translate3D(0, 0, 0) scale(1)', + backgroundColor: tokens.colorBrandBackground, + }, +}); + +export const Default = () => { + const styles = useStyles(); + + const [open, setOpen] = React.useState(false); + const motion = useMotion({ + presence: open, + }); + + return ( +
+ + +
+ Lorem ipsum +
+
+ ); +}; diff --git a/packages/react-components/react-motion-preview/stories/useMotion/UseMotionDescription.md b/packages/react-components/react-motion-preview/stories/useMotion/UseMotionDescription.md new file mode 100644 index 0000000000000..e69de29bb2d1d 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 37848d3587eff..9331a19ccef28 100644 --- a/packages/react-components/react-utilities/etc/react-utilities.api.md +++ b/packages/react-components/react-utilities/etc/react-utilities.api.md @@ -332,7 +332,7 @@ export const useIsomorphicLayoutEffect: typeof React_2.useEffect; export function useIsSSR(): boolean; // @public -export function useMergedRefs(...refs: (React_2.Ref | undefined)[]): RefObjectFunction; +export function useMergedRefs(...refs: (React_2.Ref | RefObjectFunction | undefined)[]): RefObjectFunction; // @internal (undocumented) export type UseOnClickOrScrollOutsideOptions = { diff --git a/packages/react-components/react-utilities/src/hooks/index.ts b/packages/react-components/react-utilities/src/hooks/index.ts index 018e521c02bb6..db264bf045b17 100644 --- a/packages/react-components/react-utilities/src/hooks/index.ts +++ b/packages/react-components/react-utilities/src/hooks/index.ts @@ -11,3 +11,4 @@ export * from './useOnScrollOutside'; export * from './usePrevious'; export * from './useScrollbarWidth'; export * from './useTimeout'; +export * from './useAnimationFrame'; diff --git a/packages/react-components/react-utilities/src/hooks/useMergedRefs.ts b/packages/react-components/react-utilities/src/hooks/useMergedRefs.ts index b2ae3599b3faf..a8366ce33b21d 100644 --- a/packages/react-components/react-utilities/src/hooks/useMergedRefs.ts +++ b/packages/react-components/react-utilities/src/hooks/useMergedRefs.ts @@ -12,7 +12,7 @@ export type RefObjectFunction = React.RefObject & ((value: T) => void); * @param refs - Refs to collectively update with one ref value. * @returns A function with an attached "current" prop, so that it can be treated like a RefObject. */ -export function useMergedRefs(...refs: (React.Ref | undefined)[]): RefObjectFunction { +export function useMergedRefs(...refs: (React.Ref | RefObjectFunction | undefined)[]): RefObjectFunction { const mergedCallback: RefObjectFunction = React.useCallback( (value: T) => { // Update the "current" prop hanging on the function. diff --git a/yarn.lock b/yarn.lock index 2a174e8a2c708..e7af786f84a0d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1832,6 +1832,34 @@ "@griffel/react" "^1.0.0" tslib "^2.1.0" +"@fluentui/react-jsx-runtime@^9.0.13": + version "9.0.13" + resolved "https://registry.yarnpkg.com/@fluentui/react-jsx-runtime/-/react-jsx-runtime-9.0.13.tgz#0695d11256929be07a8a585c5e793e78244a84f1" + integrity sha512-+RZ58VA8n8cPoNqKSgM42Bz5n/opqnPzb3458D8Pv4htD+rpVl6Y0ubQLMfIwN+sDqecGi8LUvQtT+UYOGCKuw== + dependencies: + "@fluentui/react-utilities" "^9.14.0" + "@swc/helpers" "^0.5.1" + +"@fluentui/react-motion-preview@0.0.0": + version "0.2.10" + resolved "https://registry.yarnpkg.com/@fluentui/react-motion-preview/-/react-motion-preview-0.2.10.tgz#35584caaf71e66a067f1f281a47e1992e87d0a66" + integrity sha512-8vc83HqT7TQEczd7TjKvVFNsvBt+5IvayVOLOFqVJ5pUF6WDFZ8q1cdgGgPrQ4nHgptIjyNqhMwCL4nvPbr9LA== + dependencies: + "@fluentui/react-jsx-runtime" "^9.0.13" + "@fluentui/react-shared-contexts" "^9.9.2" + "@fluentui/react-theme" "^9.1.14" + "@fluentui/react-utilities" "^9.14.0" + "@griffel/react" "^1.5.14" + "@swc/helpers" "^0.5.1" + +"@fluentui/react-utilities@^9.14.0": + version "9.14.0" + resolved "https://registry.yarnpkg.com/@fluentui/react-utilities/-/react-utilities-9.14.0.tgz#3a509255bdd264829947fd823f3422415d1ad262" + integrity sha512-oH/0uhbBwldckg+ZjD7l9FRKGJaBn/ptt2G+aBMMv510njgvSZjlscbN1Mfm89UTK68onsw/SOXGgessjc1tJA== + dependencies: + "@fluentui/keyboard-keys" "^9.0.6" + "@swc/helpers" "^0.5.1" + "@gar/promisify@^1.1.3": version "1.1.3" resolved "https://registry.yarnpkg.com/@gar/promisify/-/promisify-1.1.3.tgz#555193ab2e3bb3b6adc3d551c9c030d9e860daf6"