diff --git a/change/@fluentui-react-drawer-cc6d89d5-c275-456d-83da-844898335d2e.json b/change/@fluentui-react-drawer-cc6d89d5-c275-456d-83da-844898335d2e.json new file mode 100644 index 00000000000000..976f7e2cbf7789 --- /dev/null +++ b/change/@fluentui-react-drawer-cc6d89d5-c275-456d-83da-844898335d2e.json @@ -0,0 +1,7 @@ +{ + "type": "prerelease", + "comment": "feat: add motion for Drawer", + "packageName": "@fluentui/react-drawer", + "email": "marcosvmmoura@gmail.com", + "dependentChangeType": "patch" +} diff --git a/packages/react-components/react-drawer/etc/react-drawer.api.md b/packages/react-components/react-drawer/etc/react-drawer.api.md index 23284d4c1ac322..d5b87f4109689b 100644 --- a/packages/react-components/react-drawer/etc/react-drawer.api.md +++ b/packages/react-components/react-drawer/etc/react-drawer.api.md @@ -13,6 +13,8 @@ import { DialogSurfaceProps } from '@fluentui/react-dialog'; import { DialogSurfaceSlots } from '@fluentui/react-dialog'; import { DialogTitleSlots } from '@fluentui/react-dialog'; import type { ForwardRefComponent } from '@fluentui/react-utilities'; +import { MotionShorthand } from '@fluentui/react-motion-preview'; +import { MotionState } from '@fluentui/react-motion-preview'; import * as React_2 from 'react'; import type { Slot } from '@fluentui/react-utilities'; import type { SlotClassNames } from '@fluentui/react-utilities'; @@ -126,7 +128,7 @@ export type DrawerInlineSlots = { }; // @public -export type DrawerInlineState = ComponentState & DrawerInlineProps; +export type DrawerInlineState = Required & DrawerBaseState & Pick>; // @public export const DrawerOverlay: ForwardRefComponent; @@ -143,12 +145,13 @@ export type DrawerOverlaySlots = DialogSurfaceSlots & { }; // @public -export type DrawerOverlayState = ComponentState & DrawerBaseProps & { +export type DrawerOverlayState = Required, 'backdrop'> & DrawerBaseState & { dialog: DialogProps; -}; + backdropMotion: MotionState; +}>; // @public -export type DrawerProps = ComponentProps> & { +export type DrawerProps = ComponentProps & { type?: 'inline' | 'overlay'; }; @@ -182,7 +185,7 @@ export const renderDrawerHeaderTitle_unstable: (state: DrawerHeaderTitleState) = export const renderDrawerInline_unstable: (state: DrawerInlineState) => JSX.Element | null; // @public -export const renderDrawerOverlay_unstable: (state: DrawerOverlayState) => JSX.Element; +export const renderDrawerOverlay_unstable: (state: DrawerOverlayState) => JSX.Element | null; // @public export const useDrawer_unstable: (props: DrawerProps, ref: React_2.Ref) => DrawerState; @@ -218,13 +221,13 @@ export const useDrawerHeaderTitle_unstable: (props: DrawerHeaderTitleProps, ref: export const useDrawerHeaderTitleStyles_unstable: (state: DrawerHeaderTitleState) => DrawerHeaderTitleState; // @public -export const useDrawerInline_unstable: (props: DrawerInlineProps, ref: React_2.Ref) => DrawerInlineState; +export const useDrawerInline_unstable: (props: DrawerInlineProps, ref: React_2.Ref) => DrawerInlineState; // @public export const useDrawerInlineStyles_unstable: (state: DrawerInlineState) => DrawerInlineState; // @public -export const useDrawerOverlay_unstable: (props: DrawerOverlayProps, ref: React_2.Ref) => DrawerOverlayState; +export const useDrawerOverlay_unstable: (props: DrawerOverlayProps, ref: React_2.Ref) => DrawerOverlayState; // @public export const useDrawerOverlayStyles_unstable: (state: DrawerOverlayState) => DrawerOverlayState; diff --git a/packages/react-components/react-drawer/package.json b/packages/react-components/react-drawer/package.json index 7dbc8cbcaea9d8..f156002058a9aa 100644 --- a/packages/react-components/react-drawer/package.json +++ b/packages/react-components/react-drawer/package.json @@ -37,6 +37,7 @@ "dependencies": { "@fluentui/react-dialog": "^9.6.0", "@fluentui/react-jsx-runtime": "^9.0.3", + "@fluentui/react-motion-preview": "^0.1.0", "@fluentui/react-shared-contexts": "^9.7.2", "@fluentui/react-theme": "^9.1.11", "@fluentui/react-utilities": "^9.13.0", diff --git a/packages/react-components/react-drawer/src/components/Drawer/Drawer.types.ts b/packages/react-components/react-drawer/src/components/Drawer/Drawer.types.ts index 65750604e41263..4bb1f1b52683e5 100644 --- a/packages/react-components/react-drawer/src/components/Drawer/Drawer.types.ts +++ b/packages/react-components/react-drawer/src/components/Drawer/Drawer.types.ts @@ -12,7 +12,7 @@ export type DrawerSlots = { /** * Drawer Props */ -export type DrawerProps = ComponentProps> & { +export type DrawerProps = ComponentProps & { /** * Type of the drawer. * @default overlay diff --git a/packages/react-components/react-drawer/src/components/Drawer/useDrawer.ts b/packages/react-components/react-drawer/src/components/Drawer/useDrawer.ts index 59f1284df97cd3..7f69891e809cb8 100644 --- a/packages/react-components/react-drawer/src/components/Drawer/useDrawer.ts +++ b/packages/react-components/react-drawer/src/components/Drawer/useDrawer.ts @@ -17,16 +17,21 @@ import { DrawerInline } from '../DrawerInline/DrawerInline'; export const useDrawer_unstable = (props: DrawerProps, ref: React.Ref): DrawerState => { const { type = 'overlay' } = props; + const elementType = type === 'overlay' ? DrawerOverlay : DrawerInline; + return { components: { - root: type === 'overlay' ? DrawerOverlay : DrawerInline, + root: elementType, }, - root: slot.always(props, { - defaultProps: { + root: slot.always( + slot.resolveShorthand({ ref, - } as DrawerProps, - elementType: type === 'overlay' ? DrawerOverlay : DrawerInline, - }), + ...props, + }), + { + elementType, + }, + ), }; }; diff --git a/packages/react-components/react-drawer/src/components/DrawerInline/DrawerInline.test.tsx b/packages/react-components/react-drawer/src/components/DrawerInline/DrawerInline.test.tsx index 958d18df2fdb12..1e98768689f2c2 100644 --- a/packages/react-components/react-drawer/src/components/DrawerInline/DrawerInline.test.tsx +++ b/packages/react-components/react-drawer/src/components/DrawerInline/DrawerInline.test.tsx @@ -10,6 +10,8 @@ describe('DrawerInline', () => { requiredProps: { open: true, }, + // Disabled as this component returns null when not open by default + disabledTests: ['make-styles-overrides-win'], }); // TODO add more tests here, and create visual regression tests in /apps/vr-tests @@ -19,6 +21,11 @@ describe('DrawerInline', () => { expect(result.container).toMatchInlineSnapshot(`
`); }); + it('renders an closed inline drawer', () => { + const result = render(Default Drawer); + expect(result.container).toMatchInlineSnapshot(`
`); + }); + it('renders an open inline drawer', () => { const result = render(Default Drawer); expect(result.container).toMatchInlineSnapshot(` diff --git a/packages/react-components/react-drawer/src/components/DrawerInline/DrawerInline.tsx b/packages/react-components/react-drawer/src/components/DrawerInline/DrawerInline.tsx index e96cdcd7329e16..b56b3617ae1645 100644 --- a/packages/react-components/react-drawer/src/components/DrawerInline/DrawerInline.tsx +++ b/packages/react-components/react-drawer/src/components/DrawerInline/DrawerInline.tsx @@ -6,7 +6,7 @@ import type { DrawerInlineProps } from './DrawerInline.types'; import type { ForwardRefComponent } from '@fluentui/react-utilities'; /** - * DrawerInline is often used for navigation that is not dissmissable. As it is on the same level as + * DrawerInline is often used for navigation that is not dismissible. As it is on the same level as * the main surface, users can still interact with other UI elements. */ export const DrawerInline: ForwardRefComponent = React.forwardRef((props, ref) => { diff --git a/packages/react-components/react-drawer/src/components/DrawerInline/DrawerInline.types.ts b/packages/react-components/react-drawer/src/components/DrawerInline/DrawerInline.types.ts index 5d3c963be6e513..a5b3ae9ca1029e 100644 --- a/packages/react-components/react-drawer/src/components/DrawerInline/DrawerInline.types.ts +++ b/packages/react-components/react-drawer/src/components/DrawerInline/DrawerInline.types.ts @@ -1,5 +1,5 @@ import type { ComponentProps, ComponentState, Slot } from '@fluentui/react-utilities'; -import { DrawerBaseProps } from '../../util/DrawerBase.types'; +import { DrawerBaseProps, DrawerBaseState } from '../../util/DrawerBase.types'; export type DrawerInlineSlots = { root: Slot<'div'>; @@ -21,4 +21,6 @@ export type DrawerInlineProps = ComponentProps & /** * State used in rendering DrawerInline */ -export type DrawerInlineState = ComponentState & DrawerInlineProps; +export type DrawerInlineState = Required< + ComponentState & DrawerBaseState & Pick +>; diff --git a/packages/react-components/react-drawer/src/components/DrawerInline/renderDrawerInline.tsx b/packages/react-components/react-drawer/src/components/DrawerInline/renderDrawerInline.tsx index 287df0ecbe16d0..21ca121975a66b 100644 --- a/packages/react-components/react-drawer/src/components/DrawerInline/renderDrawerInline.tsx +++ b/packages/react-components/react-drawer/src/components/DrawerInline/renderDrawerInline.tsx @@ -9,10 +9,11 @@ import type { DrawerInlineState, DrawerInlineSlots } from './DrawerInline.types' * Render the final JSX of DrawerInline */ export const renderDrawerInline_unstable = (state: DrawerInlineState) => { - assertSlots(state); - - if (!state.open) { + if (!state.motion.canRender) { return null; } + + assertSlots(state); + return ; }; diff --git a/packages/react-components/react-drawer/src/components/DrawerInline/useDrawerInline.ts b/packages/react-components/react-drawer/src/components/DrawerInline/useDrawerInline.ts index d4dd0c8ad86799..e70ffb900951bb 100644 --- a/packages/react-components/react-drawer/src/components/DrawerInline/useDrawerInline.ts +++ b/packages/react-components/react-drawer/src/components/DrawerInline/useDrawerInline.ts @@ -1,7 +1,9 @@ import * as React from 'react'; -import { getNativeElementProps, useControllableState, slot } from '@fluentui/react-utilities'; +import { getNativeElementProps, useControllableState, slot, useMergedRefs } from '@fluentui/react-utilities'; +import { useMotion } from '@fluentui/react-motion-preview'; + import type { DrawerInlineProps, DrawerInlineState } from './DrawerInline.types'; -import { getDefaultDrawerProps } from '../../util/getDefaultDrawerProps'; +import { useDrawerDefaultProps } from '../../util/useDrawerDefaultProps'; /** * Create the state required to render DrawerInline. @@ -12,16 +14,21 @@ import { getDefaultDrawerProps } from '../../util/getDefaultDrawerProps'; * @param props - props from this instance of DrawerInline * @param ref - reference to root HTMLElement of DrawerInline */ -export const useDrawerInline_unstable = (props: DrawerInlineProps, ref: React.Ref): DrawerInlineState => { - const { open: initialOpen, defaultOpen, size, position } = getDefaultDrawerProps(props); +export const useDrawerInline_unstable = ( + props: DrawerInlineProps, + ref: React.Ref, +): DrawerInlineState => { + const { size, position, ...defaultProps } = useDrawerDefaultProps(props); const { separator = false } = props; const [open] = useControllableState({ - state: initialOpen, - defaultState: defaultOpen, + state: defaultProps.open, + defaultState: defaultProps.defaultOpen, initialState: false, }); + const motion = useMotion(open); + return { components: { root: 'div', @@ -29,15 +36,15 @@ export const useDrawerInline_unstable = (props: DrawerInlineProps, ref: React.Re root: slot.always( getNativeElementProps('div', { - ref, ...props, + ref: useMergedRefs(ref, motion.ref), }), { elementType: 'div' }, ), size, position, - open, separator, + motion, }; }; diff --git a/packages/react-components/react-drawer/src/components/DrawerInline/useDrawerInlineStyles.styles.ts b/packages/react-components/react-drawer/src/components/DrawerInline/useDrawerInlineStyles.styles.ts index 7751f6f95b41fc..b35fe77ae0ddd7 100644 --- a/packages/react-components/react-drawer/src/components/DrawerInline/useDrawerInlineStyles.styles.ts +++ b/packages/react-components/react-drawer/src/components/DrawerInline/useDrawerInlineStyles.styles.ts @@ -1,10 +1,11 @@ import * as React from 'react'; import { makeStyles, mergeClasses, shorthands } from '@griffel/react'; -import type { DrawerInlineSlots, DrawerInlineState } from './DrawerInline.types'; import type { SlotClassNames } from '@fluentui/react-utilities'; -import { useDrawerBaseStyles } from '../../util/useDrawerBaseStyles.styles'; import { tokens } from '@fluentui/react-theme'; +import type { DrawerInlineSlots, DrawerInlineState } from './DrawerInline.types'; +import { drawerCSSVars, useDrawerBaseClassNames } from '../../util/useDrawerBaseStyles.styles'; + export const drawerInlineClassNames: SlotClassNames = { root: 'fui-DrawerInline', }; @@ -12,17 +13,35 @@ export const drawerInlineClassNames: SlotClassNames = { /** * Styles for the root slot */ -const useStyles = makeStyles({ +const separatorValues = ['1px', 'solid', tokens.colorNeutralBackground3] as const; +const useDrawerRootStyles = makeStyles({ root: { position: 'relative', + opacity: 0, + transitionProperty: 'opacity, transform', + willChange: 'opacity, transform', }, /* Separator */ separatorStart: { - ...shorthands.borderRight('1px', 'solid', tokens.colorNeutralBackground3), + ...shorthands.borderRight(...separatorValues), }, separatorEnd: { - ...shorthands.borderLeft('1px', 'solid', tokens.colorNeutralBackground3), + ...shorthands.borderLeft(...separatorValues), + }, + + /* Positioning */ + start: { + transform: `translate3D(calc(var(${drawerCSSVars.drawerSizeVar}) * -1), 0, 0)`, + }, + end: { + transform: `translate3D(var(${drawerCSSVars.drawerSizeVar}), 0, 0)`, + }, + + /* Visible */ + visible: { + opacity: 1, + transform: `translate3D(0, 0, 0)`, }, }); @@ -30,24 +49,24 @@ const useStyles = makeStyles({ * Apply styling to the DrawerInline slots based on the state */ export const useDrawerInlineStyles_unstable = (state: DrawerInlineState): DrawerInlineState => { - const baseStyles = useDrawerBaseStyles(); - const styles = useStyles(); + const baseClassNames = useDrawerBaseClassNames(state); + const rootStyles = useDrawerRootStyles(); const separatorClass = React.useMemo(() => { if (!state.separator) { return undefined; } - return state.position === 'start' ? styles.separatorStart : styles.separatorEnd; - }, [state.position, state.separator, styles.separatorEnd, styles.separatorStart]); + return state.position === 'start' ? rootStyles.separatorStart : rootStyles.separatorEnd; + }, [state.position, state.separator, rootStyles.separatorEnd, rootStyles.separatorStart]); state.root.className = mergeClasses( drawerInlineClassNames.root, - baseStyles.root, - styles.root, - state.size && baseStyles[state.size], - state.position && baseStyles[state.position], + baseClassNames, + rootStyles.root, separatorClass, + rootStyles[state.position], + state.motion.active && rootStyles.visible, state.root.className, ); diff --git a/packages/react-components/react-drawer/src/components/DrawerOverlay/DrawerOverlay.types.ts b/packages/react-components/react-drawer/src/components/DrawerOverlay/DrawerOverlay.types.ts index db4aa3e5476322..eba829b4626dcb 100644 --- a/packages/react-components/react-drawer/src/components/DrawerOverlay/DrawerOverlay.types.ts +++ b/packages/react-components/react-drawer/src/components/DrawerOverlay/DrawerOverlay.types.ts @@ -1,6 +1,7 @@ import { DialogProps, DialogSurfaceProps, DialogSurfaceSlots } from '@fluentui/react-dialog'; import type { ComponentProps, ComponentState, Slot } from '@fluentui/react-utilities'; -import { DrawerBaseProps } from '../../util/DrawerBase.types'; +import type { MotionState } from '@fluentui/react-motion-preview'; +import type { DrawerBaseProps, DrawerBaseState } from '../../util/DrawerBase.types'; export type DrawerOverlaySlots = DialogSurfaceSlots & { root: Slot; @@ -16,7 +17,10 @@ export type DrawerOverlayProps = ComponentProps & /** * State used in rendering DrawerOverlay */ -export type DrawerOverlayState = ComponentState & - DrawerBaseProps & { - dialog: DialogProps; - }; +export type DrawerOverlayState = Required< + Omit, 'backdrop'> & + DrawerBaseState & { + dialog: DialogProps; + backdropMotion: MotionState; + } +>; diff --git a/packages/react-components/react-drawer/src/components/DrawerOverlay/renderDrawerOverlay.tsx b/packages/react-components/react-drawer/src/components/DrawerOverlay/renderDrawerOverlay.tsx index 22332028c23c3d..ef7255a11979ba 100644 --- a/packages/react-components/react-drawer/src/components/DrawerOverlay/renderDrawerOverlay.tsx +++ b/packages/react-components/react-drawer/src/components/DrawerOverlay/renderDrawerOverlay.tsx @@ -10,6 +10,10 @@ import { Dialog } from '@fluentui/react-dialog'; * Render the final JSX of DrawerOverlay */ export const renderDrawerOverlay_unstable = (state: DrawerOverlayState) => { + if (!state.motion.canRender) { + return null; + } + assertSlots(state); return ( diff --git a/packages/react-components/react-drawer/src/components/DrawerOverlay/useDrawerOverlay.ts b/packages/react-components/react-drawer/src/components/DrawerOverlay/useDrawerOverlay.ts index 7ca632f96c9011..42bd4c1ef141e2 100644 --- a/packages/react-components/react-drawer/src/components/DrawerOverlay/useDrawerOverlay.ts +++ b/packages/react-components/react-drawer/src/components/DrawerOverlay/useDrawerOverlay.ts @@ -1,8 +1,9 @@ import * as React from 'react'; -import { getNativeElementProps, slot } from '@fluentui/react-utilities'; +import { getNativeElementProps, slot, useMergedRefs } from '@fluentui/react-utilities'; import type { DrawerOverlayProps, DrawerOverlayState } from './DrawerOverlay.types'; -import { DialogProps, DialogSurface } from '@fluentui/react-dialog'; -import { getDefaultDrawerProps } from '../../util/getDefaultDrawerProps'; +import { DialogProps, DialogSurface, DialogSurfaceProps } from '@fluentui/react-dialog'; +import { useDrawerDefaultProps } from '../../util/useDrawerDefaultProps'; +import { useMotion } from '@fluentui/react-motion-preview'; /** * Create the state required to render DrawerOverlay. @@ -11,37 +12,64 @@ import { getDefaultDrawerProps } from '../../util/getDefaultDrawerProps'; * before being passed to renderDrawerOverlay_unstable. * * @param props - props from this instance of DrawerOverlay - * @param ref - reference to root HTMLElement of DrawerOverlay + * @param ref - reference to root HTMLDivElement of DrawerOverlay */ export const useDrawerOverlay_unstable = ( props: DrawerOverlayProps, - ref: React.Ref, + ref: React.Ref, ): DrawerOverlayState => { - const { open, defaultOpen, size, position } = getDefaultDrawerProps(props); + const { open, defaultOpen, size, position } = useDrawerDefaultProps(props); const { modalType = 'modal', inertTrapFocus, onOpenChange } = props; - return { - components: { - root: DialogSurface, - backdrop: 'div', + const drawerMotion = useMotion(open); + const backdropMotion = useMotion(open); + + const hasCustomBackdrop = modalType !== 'non-modal' && props.backdrop !== null; + + const root = slot.always( + getNativeElementProps('div', { + ...props, + ref: useMergedRefs(ref, drawerMotion.ref), + }), + { + elementType: DialogSurface, + defaultProps: { + backdrop: slot.optional(props.backdrop, { + elementType: 'div', + renderByDefault: hasCustomBackdrop, + defaultProps: { + ref: backdropMotion.ref, + }, + }), + }, }, + ); - root: slot.always( - getNativeElementProps('div', { - ref, - ...props, - }), - { elementType: DialogSurface }, - ), - dialog: { - open, + const dialog = slot.always( + { + open: true, defaultOpen, onOpenChange, inertTrapFocus, modalType, } as DialogProps, + { + elementType: 'div', + }, + ); + + return { + components: { + root: DialogSurface, + backdrop: 'div', + }, + + root, + dialog, size, position, + motion: drawerMotion, + backdropMotion, }; }; diff --git a/packages/react-components/react-drawer/src/components/DrawerOverlay/useDrawerOverlayStyles.styles.ts b/packages/react-components/react-drawer/src/components/DrawerOverlay/useDrawerOverlayStyles.styles.ts index 737bccaa0439cf..d93379ec7b2fb6 100644 --- a/packages/react-components/react-drawer/src/components/DrawerOverlay/useDrawerOverlayStyles.styles.ts +++ b/packages/react-components/react-drawer/src/components/DrawerOverlay/useDrawerOverlayStyles.styles.ts @@ -1,8 +1,10 @@ +import * as React from 'react'; import { makeStyles, mergeClasses } from '@griffel/react'; -import type { DrawerOverlaySlots, DrawerOverlayState } from './DrawerOverlay.types'; +import { tokens } from '@fluentui/react-theme'; import type { SlotClassNames } from '@fluentui/react-utilities'; -import { useDrawerBaseStyles } from '../../util/useDrawerBaseStyles.styles'; -import * as React from 'react'; + +import type { DrawerOverlaySlots, DrawerOverlayState } from './DrawerOverlay.types'; +import { useDrawerBaseClassNames, drawerCSSVars, useDrawerDurationStyles } from '../../util/useDrawerBaseStyles.styles'; export const drawerOverlayClassNames: SlotClassNames = { root: 'fui-DrawerOverlay', @@ -12,11 +14,46 @@ export const drawerOverlayClassNames: SlotClassNames = { /** * Styles for the root slot */ -const useStyles = makeStyles({ +const useDrawerRootStyles = makeStyles({ root: { position: 'fixed', top: 0, bottom: 0, + opacity: 0, + boxShadow: '0px transparent', + transitionProperty: 'transform, box-shadow, opacity', + willChange: 'transform, box-shadow, opacity', + }, + + /* Positioning */ + start: { + transform: `translate3D(calc(var(${drawerCSSVars.drawerSizeVar}) * -1), 0, 0)`, + }, + end: { + transform: `translate3D(calc(var(${drawerCSSVars.drawerSizeVar}) * 1), 0, 0)`, + }, + + /* Visible */ + visible: { + opacity: 1, + transform: 'translate3D(0, 0, 0)', + boxShadow: tokens.shadow64, + }, +}); + +/** + * Styles for the backdrop slot + */ +const useBackdropMotionStyles = makeStyles({ + backdrop: { + opacity: 0, + transitionProperty: 'opacity', + transitionTimingFunction: tokens.curveEasyEase, + willChange: 'opacity', + }, + + visible: { + opacity: 1, }, }); @@ -24,22 +61,30 @@ const useStyles = makeStyles({ * Apply styling to the DrawerOverlay slots based on the state */ export const useDrawerOverlayStyles_unstable = (state: DrawerOverlayState): DrawerOverlayState => { - const baseStyles = useDrawerBaseStyles(); - const styles = useStyles(); + const baseClassNames = useDrawerBaseClassNames(state); + const rootStyles = useDrawerRootStyles(); + const backdropMotionStyles = useBackdropMotionStyles(); + const durationStyles = useDrawerDurationStyles(); - const backdrop = state.root.backdrop as React.HTMLAttributes; + const backdrop = state.root.backdrop as React.HTMLAttributes | undefined; state.root.className = mergeClasses( drawerOverlayClassNames.root, - baseStyles.root, - styles.root, - state.size && baseStyles[state.size], - state.position && baseStyles[state.position], + baseClassNames, + rootStyles.root, + rootStyles[state.position], + state.motion.active && rootStyles.visible, state.root.className, ); if (backdrop) { - backdrop.className = mergeClasses(drawerOverlayClassNames.backdrop, backdrop.className); + backdrop.className = mergeClasses( + drawerOverlayClassNames.backdrop, + backdropMotionStyles.backdrop, + durationStyles[state.size], + state.backdropMotion.active && backdropMotionStyles.visible, + backdrop.className, + ); } return state; diff --git a/packages/react-components/react-drawer/src/util/DrawerBase.types.ts b/packages/react-components/react-drawer/src/util/DrawerBase.types.ts index 9e15c0aefe06a3..fd6983db4676be 100644 --- a/packages/react-components/react-drawer/src/util/DrawerBase.types.ts +++ b/packages/react-components/react-drawer/src/util/DrawerBase.types.ts @@ -1,3 +1,5 @@ +import { MotionShorthand, MotionState } from '@fluentui/react-motion-preview'; + export type DrawerBaseProps = { /** * Position of the drawer. @@ -23,7 +25,7 @@ export type DrawerBaseProps = { * * @default false */ - open?: boolean; + open?: MotionShorthand; /** * Default value for the uncontrolled open state of the Drawer. @@ -32,3 +34,7 @@ export type DrawerBaseProps = { */ defaultOpen?: boolean; }; + +export type DrawerBaseState = Required> & { + motion: MotionState; +}; diff --git a/packages/react-components/react-drawer/src/util/useDrawerBaseStyles.styles.ts b/packages/react-components/react-drawer/src/util/useDrawerBaseStyles.styles.ts index 67e59d46bf313a..5dad8d699ccc12 100644 --- a/packages/react-components/react-drawer/src/util/useDrawerBaseStyles.styles.ts +++ b/packages/react-components/react-drawer/src/util/useDrawerBaseStyles.styles.ts @@ -1,6 +1,15 @@ -import { makeStyles, shorthands } from '@griffel/react'; +import { makeStyles, mergeClasses, shorthands } from '@griffel/react'; import { tokens } from '@fluentui/react-theme'; +import { DrawerBaseState } from './DrawerBase.types'; + +/** + * CSS variable names used internally for uniform styling in Drawer. + */ +export const drawerCSSVars = { + drawerSizeVar: '--fui-Drawer--size', +}; + /** * Styles for the root slot */ @@ -11,6 +20,7 @@ export const useDrawerBaseStyles = makeStyles({ ...shorthands.borderRadius(0), ...shorthands.border(0), + width: `var(${drawerCSSVars.drawerSizeVar})`, maxWidth: '100vw', height: 'auto', boxSizing: 'border-box', @@ -21,6 +31,19 @@ export const useDrawerBaseStyles = makeStyles({ backgroundColor: tokens.colorNeutralBackground1, }, + /* Motion */ + entering: { + transitionTimingFunction: tokens.curveDecelerateMid, + }, + exiting: { + transitionTimingFunction: tokens.curveAccelerateMin, + }, + reducedMotion: { + '@media screen and (prefers-reduced-motion: reduce)': { + transitionDuration: '0.001ms', + }, + }, + /* Positioning */ start: { left: 0, @@ -33,16 +56,45 @@ export const useDrawerBaseStyles = makeStyles({ /* Sizes */ small: { - width: '320px', + [drawerCSSVars.drawerSizeVar]: '320px', }, medium: { - width: '592px', + [drawerCSSVars.drawerSizeVar]: '592px', }, large: { - width: '940px', + [drawerCSSVars.drawerSizeVar]: '940px', }, full: { - width: '100vw', - maxWidth: '100vw', + [drawerCSSVars.drawerSizeVar]: '100vw', }, }); + +export const useDrawerDurationStyles = makeStyles({ + small: { + transitionDuration: tokens.durationGentle, + }, + medium: { + transitionDuration: tokens.durationSlow, + }, + large: { + transitionDuration: tokens.durationSlower, + }, + full: { + transitionDuration: tokens.durationUltraSlow, + }, +}); + +export const useDrawerBaseClassNames = ({ position, size, motion }: DrawerBaseState) => { + const baseStyles = useDrawerBaseStyles(); + const durationStyles = useDrawerDurationStyles(); + + return mergeClasses( + baseStyles.root, + baseStyles[position], + durationStyles[size], + baseStyles[size], + baseStyles.reducedMotion, + motion.type === 'entering' && baseStyles.entering, + motion.type === 'exiting' && baseStyles.exiting, + ); +}; diff --git a/packages/react-components/react-drawer/src/util/getDefaultDrawerProps.ts b/packages/react-components/react-drawer/src/util/useDrawerDefaultProps.ts similarity index 77% rename from packages/react-components/react-drawer/src/util/getDefaultDrawerProps.ts rename to packages/react-components/react-drawer/src/util/useDrawerDefaultProps.ts index dc8547c4355f47..23d0261eb464ca 100644 --- a/packages/react-components/react-drawer/src/util/getDefaultDrawerProps.ts +++ b/packages/react-components/react-drawer/src/util/useDrawerDefaultProps.ts @@ -1,6 +1,6 @@ import { DrawerBaseProps } from './DrawerBase.types'; -export function getDefaultDrawerProps(props: DrawerBaseProps) { +export function useDrawerDefaultProps(props: DrawerBaseProps) { const { open = false, defaultOpen = false, size = 'small', position = 'start' } = props; return { diff --git a/packages/react-components/react-drawer/stories/Drawer/DrawerDefault.stories.tsx b/packages/react-components/react-drawer/stories/Drawer/DrawerDefault.stories.tsx index 27438c0e8f8d6a..5e468fb6621a8d 100644 --- a/packages/react-components/react-drawer/stories/Drawer/DrawerDefault.stories.tsx +++ b/packages/react-components/react-drawer/stories/Drawer/DrawerDefault.stories.tsx @@ -63,8 +63,8 @@ export const Default = () => {
-
diff --git a/packages/react-components/react-drawer/stories/Drawer/DrawerInline.stories.tsx b/packages/react-components/react-drawer/stories/Drawer/DrawerInline.stories.tsx index 78594afafc8900..f343d39d4e860e 100644 --- a/packages/react-components/react-drawer/stories/Drawer/DrawerInline.stories.tsx +++ b/packages/react-components/react-drawer/stories/Drawer/DrawerInline.stories.tsx @@ -7,6 +7,7 @@ const useStyles = makeStyles({ root: { ...shorthands.border('2px', 'solid', '#ccc'), ...shorthands.overflow('hidden'), + display: 'flex', height: '480px', backgroundColor: '#fff', @@ -15,10 +16,25 @@ const useStyles = makeStyles({ content: { ...shorthands.flex(1), ...shorthands.padding('16px'), + ...shorthands.overflow('auto'), + + position: 'relative', + }, + + buttons: { + ...shorthands.flex(1), + ...shorthands.padding('16px'), + + position: 'sticky', + top: '-16px', + right: '-16px', + left: '-16px', display: 'flex', justifyContent: 'center', alignItems: 'flex-start', columnGap: tokens.spacingHorizontalXS, + backgroundColor: '#fff', + transitionDuration: tokens.durationFast, }, }); @@ -52,13 +68,23 @@ export const Inline = () => {
- +
+ + + +
- + {Array.from({ length: 100 }, (_, i) => ( +

+ Lorem, ipsum dolor sit amet consectetur adipisicing elit. Tempore voluptatem similique reiciendis, ipsa + accusamus distinctio dolorum quisquam, tenetur minima animi autem nobis. Molestias totam natus, deleniti nam + itaque placeat quisquam! +

+ ))}
diff --git a/packages/react-components/react-drawer/stories/Drawer/DrawerResizable.stories.tsx b/packages/react-components/react-drawer/stories/Drawer/DrawerResizable.stories.tsx index 0235063cab2def..e489bd3d98adc5 100644 --- a/packages/react-components/react-drawer/stories/Drawer/DrawerResizable.stories.tsx +++ b/packages/react-components/react-drawer/stories/Drawer/DrawerResizable.stories.tsx @@ -12,7 +12,13 @@ const useStyles = makeStyles({ backgroundColor: '#fff', }, - drawerResizer: { + drawer: { + willChange: 'width', + transitionProperty: 'width', + transitionDuration: '16.666ms', // 60fps + }, + + resizer: { ...shorthands.borderRight('1px', 'solid', tokens.colorNeutralBackground5), width: '8px', @@ -28,7 +34,7 @@ const useStyles = makeStyles({ }, }, - drawerResizing: { + resizerActive: { borderRightWidth: '4px', borderRightColor: tokens.colorNeutralBackground5Pressed, }, @@ -42,6 +48,7 @@ const useStyles = makeStyles({ export const Resizable = () => { const styles = useStyles(); + const animationFrame = React.useRef(0); const sidebarRef = React.useRef(null); const [isResizing, setIsResizing] = React.useState(false); const [sidebarWidth, setSidebarWidth] = React.useState(320); @@ -51,7 +58,7 @@ export const Resizable = () => { const resize = React.useCallback( ({ clientX }) => { - requestAnimationFrame(() => { + animationFrame.current = requestAnimationFrame(() => { if (isResizing && sidebarRef.current) { setSidebarWidth(clientX - sidebarRef.current.getBoundingClientRect().left); } @@ -65,6 +72,7 @@ export const Resizable = () => { window.addEventListener('mouseup', stopResizing); return () => { + cancelAnimationFrame(animationFrame.current); window.removeEventListener('mousemove', resize); window.removeEventListener('mouseup', stopResizing); }; @@ -72,11 +80,14 @@ export const Resizable = () => { return (
- e.preventDefault()}> -
+ e.preventDefault()} + > +
Default Drawer diff --git a/packages/react-components/react-drawer/stories/Drawer/DrawerWithNavigation.stories.tsx b/packages/react-components/react-drawer/stories/Drawer/DrawerWithNavigation.stories.tsx index 842c49d85b92b2..4edc00cdb68b6e 100644 --- a/packages/react-components/react-drawer/stories/Drawer/DrawerWithNavigation.stories.tsx +++ b/packages/react-components/react-drawer/stories/Drawer/DrawerWithNavigation.stories.tsx @@ -7,10 +7,12 @@ import { DrawerHeaderTitle, } from '@fluentui/react-drawer'; import { Button, Toolbar, ToolbarGroup, ToolbarButton, makeStyles } from '@fluentui/react-components'; -import { Dismiss24Regular } from '@fluentui/react-icons'; -import { ArrowClockwise24Regular } from '@fluentui/react-icons'; -import { Settings24Regular } from '@fluentui/react-icons'; -import { ArrowLeft24Regular } from '@fluentui/react-icons'; +import { + Dismiss24Regular, + ArrowClockwise24Regular, + Settings24Regular, + ArrowLeft24Regular, +} from '@fluentui/react-icons'; const useStyles = makeStyles({ toolbar: { diff --git a/packages/react-components/react-drawer/stories/Drawer/DrawerWithScroll.stories.tsx b/packages/react-components/react-drawer/stories/Drawer/DrawerWithScroll.stories.tsx index 9c8f287601445d..643ade0c40d39b 100644 --- a/packages/react-components/react-drawer/stories/Drawer/DrawerWithScroll.stories.tsx +++ b/packages/react-components/react-drawer/stories/Drawer/DrawerWithScroll.stories.tsx @@ -1,16 +1,18 @@ import * as React from 'react'; import { DrawerInline, DrawerBody, DrawerHeader, DrawerHeaderTitle, DrawerFooter } from '@fluentui/react-drawer'; -import { Button, makeStyles } from '@fluentui/react-components'; +import { Button, makeStyles, tokens } from '@fluentui/react-components'; const useStyles = makeStyles({ root: { display: 'flex', - rowGap: '24px', - columnGap: '24px', + flexWrap: 'wrap', + rowGap: tokens.spacingHorizontalXXL, + columnGap: tokens.spacingHorizontalXXL, }, drawer: { height: '400px', + minWidth: '320px', }, });