Skip to content

Commit

Permalink
chore(Dialog): migrate to new motion APIs (microsoft#31380)
Browse files Browse the repository at this point in the history
Co-authored-by: Bernardo Sunderhus <bernardo.sunderhus@gmail.com>
  • Loading branch information
2 people authored and miroslavstastny committed Jun 14, 2024
1 parent 9455bcc commit 963c618
Show file tree
Hide file tree
Showing 16 changed files with 155 additions and 138 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "patch",
"comment": "chore(Dialog): migrate to new motion APIs",
"packageName": "@fluentui/react-dialog",
"email": "olfedias@microsoft.com",
"dependentChangeType": "patch"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "patch",
"comment": "fix(OverlayDrawer): unlock scroll when reduced motion is enabled",
"packageName": "@fluentui/react-drawer",
"email": "olfedias@microsoft.com",
"dependentChangeType": "patch"
}
10 changes: 10 additions & 0 deletions packages/react-components/react-dialog/etc/react-dialog.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,12 @@ export type DialogContextValue = {
requestOpenChange: (data: DialogOpenChangeData) => void;
} & Partial<ReturnType<typeof useModalAttributes>>;

// @public (undocumented)
export type DialogContextValues = {
dialog: DialogContextValue;
dialogSurface: DialogSurfaceContextValue;
};

// @public (undocumented)
export type DialogOpenChangeData = {
type: 'escapeKeyDown';
Expand Down Expand Up @@ -167,6 +173,7 @@ export type DialogSurfaceSlots = {

// @public
export type DialogSurfaceState = ComponentState<DialogSurfaceSlots> & Pick<DialogContextValue, 'isNestedDialog'> & Pick<PortalProps, 'mountNode'> & {
open?: boolean;
transitionStatus?: 'entering' | 'entered' | 'idle' | 'exiting' | 'exited' | 'unmounted';
};

Expand Down Expand Up @@ -255,6 +262,9 @@ export const useDialogContentStyles_unstable: (state: DialogContentState) => Dia
// @public (undocumented)
export const useDialogContext_unstable: <T>(selector: ContextSelector<DialogContextValue, T>) => T;

// @public (undocumented)
export function useDialogContextValues_unstable(state: DialogState): DialogContextValues;

// @public
export const useDialogSurface_unstable: (props: DialogSurfaceProps, ref: React_2.Ref<DialogSurfaceElement>) => DialogSurfaceState;

Expand Down
2 changes: 1 addition & 1 deletion packages/react-components/react-dialog/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,9 @@
"dependencies": {
"@fluentui/react-utilities": "^9.18.9",
"@fluentui/react-jsx-runtime": "^9.0.38",
"react-transition-group": "^4.4.1",
"@fluentui/keyboard-keys": "^9.0.7",
"@fluentui/react-context-selector": "^9.1.60",
"@fluentui/react-motions-preview": "^0.3.0",
"@fluentui/react-shared-contexts": "^9.19.0",
"@fluentui/react-aria": "^9.11.3",
"@fluentui/react-icons": "^2.0.239",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ export * from './Dialog';
export * from './Dialog.types';
export * from './renderDialog';
export * from './useDialog';
export * from './useDialogContextValues';
Original file line number Diff line number Diff line change
@@ -1,35 +1,28 @@
/** @jsxRuntime automatic */
/** @jsxImportSource @fluentui/react-jsx-runtime */

import { Transition } from 'react-transition-group';
import * as React from 'react';

import { DialogProvider, DialogSurfaceProvider } from '../../contexts';
import { DialogSurfaceMotion } from '../DialogSurfaceMotion';
import type { DialogState, DialogContextValues } from './Dialog.types';
import { DialogTransitionProvider } from '../../contexts/dialogTransitionContext';

/**
* Render the final JSX of Dialog
*/
export const renderDialog_unstable = (state: DialogState, contextValues: DialogContextValues) => {
const { content, trigger } = state;
const { content, open, trigger } = state;

return (
<DialogProvider value={contextValues.dialog}>
<DialogSurfaceProvider value={contextValues.dialogSurface}>
{trigger}
{process.env.NODE_ENV === 'test' ? (
state.open && <DialogTransitionProvider value={undefined}>{content}</DialogTransitionProvider>
) : (
<Transition
mountOnEnter
unmountOnExit
in={state.open}
nodeRef={state.dialogRef}
appear={true}
// FIXME: this should not be hardcoded tokens.durationGentle
timeout={250}
>
{status => <DialogTransitionProvider value={status}>{content}</DialogTransitionProvider>}
</Transition>
{content && (
<DialogSurfaceMotion appear visible={open} unmountOnExit>
{/* Casting here as content should be equivalent to <DialogSurface/> */}
{/* FIXME: content should not be ReactNode it should be ReactElement instead. */}
{content as React.ReactElement}
</DialogSurfaceMotion>
)}
</DialogSurfaceProvider>
</DialogProvider>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { createPresenceComponent, motionTokens } from '@fluentui/react-motions-preview';

const keyframes = [{ opacity: 0 }, { opacity: 1 }];

export const DialogBackdropMotion = createPresenceComponent({
enter: {
keyframes,
easing: motionTokens.curveLinear,
duration: motionTokens.durationGentle,
},
exit: {
keyframes: [...keyframes].reverse(),
easing: motionTokens.curveLinear,
duration: motionTokens.durationGentle,
},
});
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { ComponentProps, ComponentState, Slot } from '@fluentui/react-utilities';
import type { PortalProps } from '@fluentui/react-portal';
import type { ComponentProps, ComponentState, Slot } from '@fluentui/react-utilities';

import { DialogContextValue, DialogSurfaceContextValue } from '../../contexts';

export type DialogSurfaceSlots = {
Expand Down Expand Up @@ -35,9 +36,13 @@ export type DialogSurfaceState = ComponentState<DialogSurfaceSlots> &
// This is only partial to avoid breaking changes, it should be mandatory and in fact it is always defined internally.
Pick<DialogContextValue, 'isNestedDialog'> &
Pick<PortalProps, 'mountNode'> & {
open?: boolean;

/**
* Transition status for animation.
* In test environment, this is always `undefined`.
*
* @deprecated Will be always `undefined`.
*/
transitionStatus?: 'entering' | 'entered' | 'idle' | 'exiting' | 'exited' | 'unmounted';
};
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
/** @jsxRuntime automatic */
/** @jsxImportSource @fluentui/react-jsx-runtime */

import { Portal } from '@fluentui/react-portal';
import { assertSlots } from '@fluentui/react-utilities';
import type { DialogSurfaceState, DialogSurfaceSlots, DialogSurfaceContextValues } from './DialogSurface.types';

import { DialogSurfaceProvider } from '../../contexts';
import { Portal } from '@fluentui/react-portal';
import { DialogBackdropMotion } from '../DialogBackdropMotion';
import type { DialogSurfaceState, DialogSurfaceSlots, DialogSurfaceContextValues } from './DialogSurface.types';

/**
* Render the final JSX of DialogSurface
Expand All @@ -14,7 +16,11 @@ export const renderDialogSurface_unstable = (state: DialogSurfaceState, contextV

return (
<Portal mountNode={state.mountNode}>
{state.backdrop && <state.backdrop />}
{state.backdrop && (
<DialogBackdropMotion appear visible={state.open}>
<state.backdrop />
</DialogBackdropMotion>
)}
<DialogSurfaceProvider value={contextValues.dialogSurface}>
<state.root />
</DialogSurfaceProvider>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import * as React from 'react';
import { Escape } from '@fluentui/keyboard-keys';
import {
useEventCallback,
useMergedRefs,
Expand All @@ -7,11 +7,11 @@ import {
getIntrinsicElementProps,
useIsomorphicLayoutEffect,
} from '@fluentui/react-utilities';
import type { DialogSurfaceElement, DialogSurfaceProps, DialogSurfaceState } from './DialogSurface.types';
import * as React from 'react';

import { useDialogContext_unstable } from '../../contexts';
import { Escape } from '@fluentui/keyboard-keys';
import { useDialogTransitionContext_unstable } from '../../contexts/dialogTransitionContext';
import { useDisableBodyScroll } from '../../utils/useDisableBodyScroll';
import type { DialogSurfaceElement, DialogSurfaceProps, DialogSurfaceState } from './DialogSurface.types';

/**
* Create the state required to render DialogSurface.
Expand All @@ -28,7 +28,7 @@ export const useDialogSurface_unstable = (
): DialogSurfaceState => {
const modalType = useDialogContext_unstable(ctx => ctx.modalType);
const isNestedDialog = useDialogContext_unstable(ctx => ctx.isNestedDialog);
const transitionStatus = useDialogTransitionContext_unstable();

const modalAttributes = useDialogContext_unstable(ctx => ctx.modalAttributes);
const dialogRef = useDialogContext_unstable(ctx => ctx.dialogRef);
const requestOpenChange = useDialogContext_unstable(ctx => ctx.requestOpenChange);
Expand Down Expand Up @@ -75,23 +75,24 @@ export const useDialogSurface_unstable = (
}

const { disableBodyScroll, enableBodyScroll } = useDisableBodyScroll();
const isBodyScrollLocked = Boolean(open && modalType !== 'non-modal');

useIsomorphicLayoutEffect(() => {
if (isNestedDialog) {
if (isNestedDialog || modalType === 'non-modal') {
return;
}
if (open && isBodyScrollLocked && transitionStatus === 'entering') {
disableBodyScroll();
} else if (transitionStatus === 'exited') {

disableBodyScroll();

return () => {
enableBodyScroll();
}
}, [disableBodyScroll, enableBodyScroll, isBodyScrollLocked, isNestedDialog, open, transitionStatus]);
};
}, [enableBodyScroll, isNestedDialog, disableBodyScroll, modalType]);

return {
components: { backdrop: 'div', root: 'div' },
open,
backdrop,
isNestedDialog,
transitionStatus,
mountNode: props.mountNode,
root: slot.always(
getIntrinsicElementProps('div', {
Expand All @@ -109,5 +110,8 @@ export const useDialogSurface_unstable = (
}),
{ elementType: 'div' },
),

// Deprecated properties
transitionStatus: undefined,
};
};
Original file line number Diff line number Diff line change
Expand Up @@ -47,108 +47,44 @@ const useRootBaseStyle = makeResetStyles({
[MEDIA_QUERY_SHORT_SCREEN]: {
overflowY: 'auto',
// We need to offset the scrollbar by adding transparent borders otherwise
// it conflits with the border radius.
// it conflicts with the border radius.
paddingRight: `calc(${SURFACE_PADDING} - ${FULLSCREEN_DIALOG_SCROLLBAR_OFFSET})`,
borderRightWidth: FULLSCREEN_DIALOG_SCROLLBAR_OFFSET,
borderTopWidth: FULLSCREEN_DIALOG_SCROLLBAR_OFFSET,
borderBottomWidth: FULLSCREEN_DIALOG_SCROLLBAR_OFFSET,
},
});

const rootVisible = {
boxShadow: tokens.shadow64,
transform: 'scale(1) translateZ(0)',
opacity: 1,
};
const rootWhenAnimating = {
transitionDuration: tokens.durationGentle,
transitionProperty: 'opacity, transform, box-shadow',
// // FIXME: https://github.com/microsoft/fluentui/issues/29473
transitionTimingFunction: tokens.curveDecelerateMid,
};
const useRootStyles = makeStyles({
animated: {
// initial style before animation:
opacity: 0,
boxShadow: '0px 0px 0px 0px rgba(0, 0, 0, 0.1)',
transform: 'scale(0.85) translateZ(0)',
},
static: {
boxShadow: tokens.shadow64,
},
unmounted: {},
entering: {
...rootWhenAnimating,
...rootVisible,
},
entered: rootVisible,
idle: rootVisible,
exiting: {
...rootWhenAnimating,
transitionTimingFunction: tokens.curveAccelerateMin,
},
exited: {},
});

/**
* Styles for the backdrop slot
*/
const backdropVisible = {
opacity: 1,
};
const useBackdropBaseStyle = makeResetStyles({
inset: '0px',
backgroundColor: 'rgba(0, 0, 0, 0.4)',
position: 'fixed',

// initial style before animation:
transitionDuration: tokens.durationGentle,
transitionTimingFunction: tokens.curveLinear,
transitionProperty: 'opacity',
willChange: 'opacity',
opacity: 0,
});

const useBackdropStyles = makeStyles({
nestedDialogBackdrop: {
backgroundColor: tokens.colorTransparentBackground,
},
unmounted: {},
entering: backdropVisible,
entered: backdropVisible,
idle: backdropVisible,
exiting: {
transitionTimingFunction: tokens.curveAccelerateMin,
},
exited: {},
});

/**
* Apply styling to the DialogSurface slots based on the state
*/
export const useDialogSurfaceStyles_unstable = (state: DialogSurfaceState): DialogSurfaceState => {
const { isNestedDialog, root, backdrop, transitionStatus } = state;
const { isNestedDialog, root, backdrop } = state;

const rootBaseStyle = useRootBaseStyle();
const rootStyles = useRootStyles();

const backdropBaseStyle = useBackdropBaseStyle();
const backdropStyles = useBackdropStyles();

root.className = mergeClasses(
dialogSurfaceClassNames.root,
rootBaseStyle,
transitionStatus ? rootStyles.animated : rootStyles.static,
transitionStatus && rootStyles[transitionStatus],
root.className,
);
root.className = mergeClasses(dialogSurfaceClassNames.root, rootBaseStyle, root.className);

if (backdrop) {
backdrop.className = mergeClasses(
dialogSurfaceClassNames.backdrop,
backdropBaseStyle,
isNestedDialog && backdropStyles.nestedDialogBackdrop,
transitionStatus && backdropStyles[transitionStatus],
backdrop.className,
);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { createPresenceComponent, motionTokens } from '@fluentui/react-motions-preview';
import { tokens } from '@fluentui/react-theme';

const keyframes = [
{ opacity: 0, boxShadow: '0px 0px 0px 0px rgba(0, 0, 0, 0.1)', transform: 'scale(0.85) translateZ(0)' },
{
boxShadow: tokens.shadow64,
transform: 'scale(1) translateZ(0)',
opacity: 1,
},
];

export const DialogSurfaceMotion = createPresenceComponent({
enter: {
keyframes,
easing: motionTokens.curveDecelerateMid,
duration: motionTokens.durationGentle,
},
exit: {
keyframes: [...keyframes].reverse(),
easing: motionTokens.curveAccelerateMin,
duration: motionTokens.durationGentle,
},
});
Loading

0 comments on commit 963c618

Please sign in to comment.