Skip to content

Commit

Permalink
feat(react-drawer): make dialog slot to be used for composition only (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
marcosmoura authored Oct 6, 2023
1 parent 8eee0cd commit fd51d50
Show file tree
Hide file tree
Showing 15 changed files with 199 additions and 97 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "prerelease",
"comment": "feat: make dialog slot internal to be used for composition only",
"packageName": "@fluentui/react-drawer",
"email": "[email protected]",
"dependentChangeType": "patch"
}
16 changes: 7 additions & 9 deletions packages/react-components/react-drawer/etc/react-drawer.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,8 @@

import type { ComponentProps } from '@fluentui/react-utilities';
import type { ComponentState } from '@fluentui/react-utilities';
import { DialogProps } from '@fluentui/react-dialog';
import { DialogSurfaceProps } from '@fluentui/react-dialog';
import { DialogSurfaceSlots } from '@fluentui/react-dialog';
import type { DialogProps } from '@fluentui/react-dialog';
import type { DialogSurfaceSlots } 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';
Expand Down Expand Up @@ -131,19 +130,18 @@ export type DrawerInlineState = Required<ComponentState<DrawerInlineSlots> & Dra
export const DrawerOverlay: ForwardRefComponent<DrawerOverlayProps>;

// @public (undocumented)
export const drawerOverlayClassNames: Omit<SlotClassNames<DrawerOverlaySlots>, 'dialog'>;
export const drawerOverlayClassNames: SlotClassNames<DrawerOverlaySurfaceSlots>;

// @public
export type DrawerOverlayProps = ComponentProps<DrawerOverlaySlots> & Pick<DialogProps, 'modalType' | 'onOpenChange' | 'inertTrapFocus' | 'defaultOpen'> & DrawerBaseProps;

// @public (undocumented)
export type DrawerOverlaySlots = DialogSurfaceSlots & {
root: Slot<DialogSurfaceProps>;
dialog?: Slot<DialogProps>;
// @public
export type DrawerOverlaySlots = {
root: Slot<DrawerOverlaySurfaceProps>;
};

// @public
export type DrawerOverlayState = Omit<ComponentState<DrawerOverlaySlots>, 'backdrop'> & Required<DrawerBaseState & {
export type DrawerOverlayState = Omit<ComponentState<DrawerOverlayInternalSlots>, 'backdrop'> & Required<DrawerBaseState & {
backdropMotion: MotionState<HTMLDivElement>;
}>;

Expand Down
1 change: 1 addition & 0 deletions packages/react-components/react-drawer/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
"@fluentui/react-jsx-runtime": "^9.0.14",
"@fluentui/react-motion-preview": "^0.3.0",
"@fluentui/react-shared-contexts": "^9.9.2",
"@fluentui/react-tabster": "^9.13.4",
"@fluentui/react-theme": "^9.1.14",
"@fluentui/react-utilities": "^9.14.1",
"@griffel/react": "^1.5.14",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,7 @@ import { DrawerInline } from '../DrawerInline';
* @param ref - reference to root HTMLElement of Drawer
*/
export const useDrawer_unstable = (props: DrawerProps, ref: React.Ref<HTMLElement>): DrawerState => {
const { type = 'overlay' } = props;

const elementType = type === 'overlay' ? DrawerOverlay : DrawerInline;
const elementType = props.type === 'inline' ? DrawerInline : DrawerOverlay;

return {
components: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@ import * as React from 'react';
import { mount } from '@cypress/react';
import { FluentProvider } from '@fluentui/react-provider';
import { webLightTheme } from '@fluentui/react-theme';
import { dialogSurfaceClassNames } from '@fluentui/react-dialog';

import { testDrawerBaseScenarios } from '../../e2e/DrawerShared';
import { DrawerOverlay } from './DrawerOverlay';
import { DrawerOverlayProps } from './DrawerOverlay.types';
import { drawerOverlayClassNames } from './useDrawerOverlayStyles.styles';

const mountFluent = (element: JSX.Element) => {
mount(<FluentProvider theme={webLightTheme}>{element}</FluentProvider>);
Expand All @@ -28,14 +28,14 @@ describe('DrawerOverlay', () => {
it('should render backdrop', () => {
mountFluent(<ExampleDrawer />);

cy.get(`.${dialogSurfaceClassNames.backdrop}`).should('exist');
cy.get(`.${drawerOverlayClassNames.backdrop}`).should('exist');
});

it('should close when backdrop is clicked', () => {
mountFluent(<ExampleDrawer />);

cy.get('#drawer').should('exist');
cy.get(`.${dialogSurfaceClassNames.backdrop}`).click({ force: true });
cy.get(`.${drawerOverlayClassNames.backdrop}`).click({ force: true });
cy.get('#drawer').should('not.exist');
});
});
Expand All @@ -44,14 +44,14 @@ describe('DrawerOverlay', () => {
it('should render backdrop', () => {
mountFluent(<ExampleDrawer modalType="alert" />);

cy.get(`.${dialogSurfaceClassNames.backdrop}`).should('exist');
cy.get(`.${drawerOverlayClassNames.backdrop}`).should('exist');
});

it('should not close when backdrop is clicked', () => {
mountFluent(<ExampleDrawer modalType="alert" />);

cy.get('#drawer').should('exist');
cy.get(`.${dialogSurfaceClassNames.backdrop}`).click({ force: true });
cy.get(`.${drawerOverlayClassNames.backdrop}`).click({ force: true });
cy.get('#drawer').should('exist');
});
});
Expand All @@ -60,7 +60,7 @@ describe('DrawerOverlay', () => {
it('should not render backdrop when modalType is default', () => {
mountFluent(<ExampleDrawer modalType="non-modal" />);

cy.get(`.${dialogSurfaceClassNames.backdrop}`).should('not.exist');
cy.get(`.${drawerOverlayClassNames.backdrop}`).should('not.exist');
});
});
});
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,28 @@
import { DialogProps, DialogSurfaceProps, DialogSurfaceSlots } from '@fluentui/react-dialog';
import type { DialogProps } from '@fluentui/react-dialog';
import type { ComponentProps, ComponentState, Slot } from '@fluentui/react-utilities';
import type { MotionState } from '@fluentui/react-motion-preview';

import type { DrawerBaseProps, DrawerBaseState } from '../../shared/DrawerBase.types';
import { DrawerOverlaySurfaceProps } from './DrawerOverlaySurface';

export type DrawerOverlaySlots = DialogSurfaceSlots & {
root: Slot<DialogSurfaceProps>;
/**
* DrawerOverlay slots
*/
export type DrawerOverlaySlots = {
/**
* Slot for the root element.
*/
root: Slot<DrawerOverlaySurfaceProps>;
};

/**
* DrawerOverlay internal slots for when using with composition API
*/
export type DrawerOverlayInternalSlots = DrawerOverlaySlots & {
/**
* Slot for the dialog component that wraps the drawer.
*/
dialog?: Slot<DialogProps>;
dialog: Slot<DialogProps>;
};

/**
Expand All @@ -23,9 +35,12 @@ export type DrawerOverlayProps = ComponentProps<DrawerOverlaySlots> &
/**
* State used in rendering DrawerOverlay
*/
export type DrawerOverlayState = Omit<ComponentState<DrawerOverlaySlots>, 'backdrop'> &
export type DrawerOverlayState = Omit<ComponentState<DrawerOverlayInternalSlots>, 'backdrop'> &
Required<
DrawerBaseState & {
/**
* Motion state for the drawer backdrop.
*/
backdropMotion: MotionState<HTMLDivElement>;
}
>;
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import * as React from 'react';
import type { ForwardRefComponent } from '@fluentui/react-utilities';
import {
useDialogSurface_unstable,
useDialogSurfaceContextValues_unstable,
renderDialogSurface_unstable,
} from '@fluentui/react-dialog';

import { useDrawerOverlaySurfaceStyles_unstable } from './useDrawerOverlaySurfaceStyles.styles';
import type { DrawerOverlaySurfaceProps } from './DrawerOverlaySurface.types';

/**
* @internal
* DrawerOverlaySurface is a proxy for DialogSurface as is only meant to be used internally for Drawer.
*/
export const DrawerOverlaySurface: ForwardRefComponent<DrawerOverlaySurfaceProps> = React.forwardRef((props, ref) => {
const dialogSurfaceState = useDialogSurface_unstable(props, ref);
const dialogSurfaceContextValues = useDialogSurfaceContextValues_unstable(dialogSurfaceState);

useDrawerOverlaySurfaceStyles_unstable(dialogSurfaceState);

return renderDialogSurface_unstable(dialogSurfaceState, dialogSurfaceContextValues);
});

DrawerOverlaySurface.displayName = 'DrawerOverlaySurface';
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import type { DialogSurfaceSlots, DialogSurfaceState } from '@fluentui/react-dialog';
import type { ComponentProps, ComponentState } from '@fluentui/react-utilities';

/**
* DrawerOverlaySurface slots
*/
export type DrawerOverlaySurfaceSlots = DialogSurfaceSlots;

/**
* DrawerOverlaySurface Props
*/
export type DrawerOverlaySurfaceProps = ComponentProps<DrawerOverlaySurfaceSlots>;

/**
* State used in rendering DrawerOverlaySurface
*/
export type DrawerOverlaySurfaceState = ComponentState<DrawerOverlaySurfaceSlots> & DialogSurfaceState;
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './DrawerOverlaySurface';
export * from './DrawerOverlaySurface.types';
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { makeResetStyles, makeStyles, mergeClasses, shorthands } from '@griffel/react';
import type { SlotClassNames } from '@fluentui/react-utilities';
import { tokens } from '@fluentui/react-theme';

import type { DrawerOverlaySurfaceSlots, DrawerOverlaySurfaceState } from './DrawerOverlaySurface.types';

export const drawerOverlaySurfaceClassNames: SlotClassNames<DrawerOverlaySurfaceSlots> = {
root: 'fui-DrawerOverlaySurface',
backdrop: 'fui-DrawerOverlaySurface__backdrop',
};

/**
* Styles for the backdrop slot
*/
const useBackdropResetStyles = makeResetStyles({
...shorthands.inset('0px'),
position: 'fixed',
backgroundColor: 'rgba(0, 0, 0, 0.4)',
});

const useBackdropStyles = makeStyles({
nested: {
backgroundColor: tokens.colorTransparentBackground,
},
});

/**
* Apply styling to the DrawerOverlaySurface slots based on the state
*/
export const useDrawerOverlaySurfaceStyles_unstable = (state: DrawerOverlaySurfaceState): DrawerOverlaySurfaceState => {
const backdropResetStyles = useBackdropResetStyles();
const backdropStyles = useBackdropStyles();

if (state.backdrop) {
state.backdrop.className = mergeClasses(
backdropResetStyles,
state.isNestedDialog && backdropStyles.nested,
state.backdrop.className,
);
}

return state;
};
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,17 @@
/** @jsxImportSource @fluentui/react-jsx-runtime */
import { assertSlots } from '@fluentui/react-utilities';

import type { DrawerOverlayState, DrawerOverlaySlots } from './DrawerOverlay.types';
import type { DrawerOverlayState, DrawerOverlayInternalSlots } from './DrawerOverlay.types';

/**
* Render the final JSX of DrawerOverlay
*/
export const renderDrawerOverlay_unstable = (state: DrawerOverlayState) => {
if (!state.dialog || !state.motion.canRender) {
if (!state.motion.canRender) {
return null;
}

assertSlots<DrawerOverlaySlots>(state);
assertSlots<DrawerOverlayInternalSlots>(state);

return (
<state.dialog>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import * as React from 'react';
import { slot, useMergedRefs } from '@fluentui/react-utilities';
import { Dialog, DialogSurface } from '@fluentui/react-dialog';
import { Dialog } from '@fluentui/react-dialog';
import { useMotion } from '@fluentui/react-motion-preview';

import { useDrawerDefaultProps } from '../../shared/useDrawerDefaultProps';
import type { DrawerOverlayProps, DrawerOverlayState } from './DrawerOverlay.types';
import { DrawerOverlaySurface } from './DrawerOverlaySurface';

/**
* Create the state required to render DrawerOverlay.
Expand All @@ -25,44 +26,48 @@ export const useDrawerOverlay_unstable = (
const drawerMotion = useMotion<HTMLDivElement>(open);
const backdropMotion = useMotion<HTMLDivElement>(open);

const hasCustomBackdrop = modalType !== 'non-modal' && props.backdrop !== null;
const backdropInnerProps = slot.resolveShorthand(props.backdrop);
const hasCustomBackdrop = modalType !== 'non-modal' && backdropInnerProps !== null;

const backdropProps = {
...backdropInnerProps,
ref: useMergedRefs(backdropMotion.ref, backdropInnerProps?.ref),
};
const root = slot.always(
{
...props,
ref: useMergedRefs(ref, drawerMotion.ref),
backdrop: hasCustomBackdrop ? backdropProps : null,
},
{
elementType: DialogSurface,
elementType: DrawerOverlaySurface,
defaultProps: {
backdrop: slot.optional(props.backdrop, {
elementType: 'div',
renderByDefault: hasCustomBackdrop,
defaultProps: {
ref: backdropMotion.ref,
},
}),
ref: useMergedRefs(ref, drawerMotion.ref),
},
},
);

const dialog = slot.optional(props.dialog, {
elementType: Dialog,
renderByDefault: true,
defaultProps: {
const dialog = slot.always(
{
open: true,
defaultOpen,
onOpenChange,
inertTrapFocus,
modalType,
/*
* children is not needed here because we construct the children in the render function,
* but it's required by DialogProps
*/
children: null as unknown as JSX.Element,
},
});
{
elementType: Dialog,
},
);

return {
components: {
root: DialogSurface,
root: DrawerOverlaySurface,
dialog: Dialog,
backdrop: 'div',
},

root,
Expand Down
Loading

0 comments on commit fd51d50

Please sign in to comment.