Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 5 additions & 7 deletions packages/react-components/react-dialog/Spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,10 +84,6 @@ The root level component serves as an interface for interaction with all possibl

```tsx
type DialogSlots = {
/**
* The dialog element itself
*/
root: Slot<'div'>;
/**
* Dimmed background of dialog.
* The default overlay is rendered as a `<div>` with styling.
Expand All @@ -110,8 +106,10 @@ type DialogProps = ComponentProps<DialogSlots> & {
* `non-modal`: When a non-modal dialog is open, the rest of the page is not dimmed out and users can interact with the rest of the page. This also implies that the tab focus can move outside the dialog when it reaches the last focusable element.
*
* `alert`: is a special type of modal dialogs that interrupts the user's workflow to communicate an important message or ask for a decision. Unlike a typical modal dialog, the user must take an action through the options given to dismiss the dialog, and it cannot be dismissed through the dimmed background or escape key.
*
* @default 'modal'
*/
type?: 'modal' | 'non-modal' | 'alert';
modalType?: 'modal' | 'non-modal' | 'alert';
/**
* Controls the open state of the dialog
* @default undefined
Expand Down Expand Up @@ -152,9 +150,10 @@ export type DialogTriggerProps = {
/**
* Explicitly declare if the trigger is responsible for opening,
* closing or toggling a Dialog visibility state.
*
* @default 'toggle'
*/
type?: 'open' | 'close' | 'toggle';
action?: 'open' | 'close' | 'toggle';
/**
* Explicitly require single child or render function
* to inject properties
Expand All @@ -166,7 +165,6 @@ export type DialogTriggerProps = {
### DialogContent

The `DialogContent` component represents the visual part of a `Dialog` as a whole, it contains everything that should be visible.
By itself it has no style, but it's responsible of showing/hiding content when `Dialog` visibility state changes, also it'll ensure a `Portal` is properly created for the content being provided as well as for the `overlay` element provided by `Dialog`

```tsx
type DialogTitleSlots = {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { Dialog } from '@fluentui/react-dialog';

// eslint-disable-next-line no-console
console.log(Dialog);

export default {
name: 'Dialog',
};
2 changes: 2 additions & 0 deletions packages/react-components/react-dialog/config/tests.js
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
/** Jest test setup file. */

require('@testing-library/jest-dom');
3 changes: 3 additions & 0 deletions packages/react-components/react-dialog/cypress.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import baseConfig from '@fluentui/scripts/cypress.config';

export default baseConfig;
22 changes: 22 additions & 0 deletions packages/react-components/react-dialog/e2e/Dialog.e2e.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import * as React from 'react';
import { mount as mountBase } from '@cypress/react';

import { FluentProvider } from '@fluentui/react-provider';
import { teamsLightTheme } from '@fluentui/react-theme';

import { Dialog } from '@fluentui/react-dialog';

const mount = (element: JSX.Element) => mountBase(<FluentProvider theme={teamsLightTheme}>{element}</FluentProvider>);

describe('Dialog', () => {
it('should be closed by default', () => {
mount(
<Dialog>
<div>
<button id="close-btn">close</button>
</div>
</Dialog>,
);
cy.get('#close-btn').should('not.exist');
});
});
1 change: 1 addition & 0 deletions packages/react-components/react-dialog/e2e/selectors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const dialogSelector = '';
9 changes: 9 additions & 0 deletions packages/react-components/react-dialog/e2e/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"extends": "../tsconfig.json",
"compilerOptions": {
"isolatedModules": false,
"types": ["node", "cypress", "cypress-storybook/cypress", "cypress-real-events"],
"lib": ["ES2019", "dom"]
},
"include": ["**/*.ts", "**/*.tsx"]
}
52 changes: 43 additions & 9 deletions packages/react-components/react-dialog/etc/react-dialog.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,33 +6,67 @@

import type { ComponentProps } from '@fluentui/react-utilities';
import type { ComponentState } from '@fluentui/react-utilities';
import type { ForwardRefComponent } from '@fluentui/react-utilities';
import * as React_2 from 'react';
import type { Slot } from '@fluentui/react-utilities';
import type { SlotClassNames } from '@fluentui/react-utilities';

// @public
export const Dialog: ForwardRefComponent<DialogProps>;
export const Dialog: React_2.FC<DialogProps>;

// @public (undocumented)
export const dialogClassNames: SlotClassNames<DialogSlots>;

// @public
export type DialogProps = ComponentProps<DialogSlots>;
// @public (undocumented)
export type DialogOpenChangeData = {
type: 'escapeKeyDown';
open: boolean;
event: React_2.KeyboardEvent;
}
/**
* document escape keydown defers from internal escape keydown events because of the synthetic event API
*/
| {
type: 'documentEscapeKeyDown';
open: boolean;
event: KeyboardEvent;
} | {
type: 'overlayClick';
open: boolean;
event: React_2.MouseEvent;
} | {
type: 'triggerClick';
open: boolean;
event: React_2.MouseEvent;
};

// @public (undocumented)
export type DialogOpenChangeEvent = React_2.KeyboardEvent | React_2.MouseEvent | KeyboardEvent;

// @public (undocumented)
export type DialogProps = ComponentProps<Partial<DialogSlots>> & {
modalType?: DialogModalType;
open?: boolean;
defaultOpen?: boolean;
onOpenChange?: (event: DialogOpenChangeEvent, data: DialogOpenChangeData) => void;
children: [JSX.Element, JSX.Element] | JSX.Element;
};

// @public (undocumented)
export type DialogSlots = {
root: Slot<'div'>;
overlay?: Slot<'div'>;
};

// @public
export type DialogState = ComponentState<DialogSlots>;
// @public (undocumented)
export type DialogState = ComponentState<DialogSlots> & DialogContextValue & {
content: React_2.ReactNode;
trigger: React_2.ReactNode;
};

// @public
export const renderDialog_unstable: (state: DialogState) => JSX.Element;
export const renderDialog_unstable: (state: DialogState, contextValues: DialogContextValues) => JSX.Element;

// @public
export const useDialog_unstable: (props: DialogProps, ref: React_2.Ref<HTMLElement>) => DialogState;
export const useDialog_unstable: (props: DialogProps) => DialogState;

// @public
export const useDialogStyles_unstable: (state: DialogState) => DialogState;
Expand Down
13 changes: 12 additions & 1 deletion packages/react-components/react-dialog/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,15 @@
"build": "just-scripts build",
"clean": "just-scripts clean",
"code-style": "just-scripts code-style",
"bundle-size": "bundle-size measure",
"just": "just-scripts",
"lint": "just-scripts lint",
"start": "yarn storybook",
"test": "jest --passWithNoTests",
"e2e": "yarn cypress run --component",
"e2e:local": "yarn cypress open --component",
"docs": "api-extractor run --config=config/api-extractor.local.json --local",
"build:local": "tsc -p ./tsconfig.lib.json --module esnext --emitDeclarationOnly && node ../../../scripts/typescript/normalize-import --output ./dist/packages/react-components/react-dialog/src && yarn docs",
"build:local": "tsc -p ./tsconfig.lib.json --module esnext --emitDeclarationOnly && node ../../../scripts/typescript/normalize-import --output ./dist/types/packages/react-components/react-dialog/src && yarn docs",
"storybook": "node ../../../scripts/storybook/runner",
"type-check": "tsc -b tsconfig.json"
},
Expand All @@ -34,6 +37,14 @@
"dependencies": {
"@griffel/react": "^1.2.0",
"@fluentui/react-utilities": "^9.0.2",
"@fluentui/keyboard-keys": "^9.0.0",
"@fluentui/react-context-selector": "^9.0.2",
"@fluentui/react-shared-contexts": "^9.0.0",
"@fluentui/react-aria": "^9.0.2",
"@fluentui/react-icons": "^2.0.175",
"@fluentui/react-tabster": "^9.0.3",
"@fluentui/react-theme": "^9.0.0",
"@fluentui/react-portal": "^9.0.3",
"tslib": "^2.1.0"
},
"peerDependencies": {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,19 +1,37 @@
import * as React from 'react';
import { render } from '@testing-library/react';
import { Dialog } from './Dialog';
import { DialogProps } from './Dialog.types';
import { isConformant } from '../../common/isConformant';

describe('Dialog', () => {
isConformant({
isConformant<DialogProps>({
Component: Dialog,
displayName: 'Dialog',
disabledTests: ['component-has-static-classname-exported'],
disabledTests: [
// Dialog does not render DOM elements
'component-handles-ref',
'component-has-root-ref',
'component-handles-classname',
'component-has-static-classname',
'component-has-static-classnames-object',
'component-has-static-classname-exported',
// TODO:
// onOpenChange: A second (data) argument cannot be a union
'consistent-callback-args',
// Dialog does not have own styles
'make-styles-overrides-win',
],
});

// TODO add more tests here, and create visual regression tests in /apps/vr-tests

it('renders a default state', () => {
const result = render(<Dialog>Default Dialog</Dialog>);
const result = render(
<Dialog>
<div>Default Dialog</div>
</Dialog>,
);
expect(result.container).toMatchSnapshot();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,21 @@ import { useDialog_unstable } from './useDialog';
import { renderDialog_unstable } from './renderDialog';
import { useDialogStyles_unstable } from './useDialogStyles';
import type { DialogProps } from './Dialog.types';
import type { ForwardRefComponent } from '@fluentui/react-utilities';
import { useDialogContextValues_unstable } from './useDialogContextValues';

/**
* A Dialog is an elevated Card triggered by a user’s action.
* The `Dialog` root level component serves as an interface for interaction with all possible behaviors exposed.
* It provides context down the hierarchy to `children` compound components to allow functionality.
* This component expects to receive as children either a `DialogContent` or a `DialogTrigger`
* and a `DialogContent` (or some component that will eventually render one of those compound components)
* in this specific order
*/
export const Dialog: ForwardRefComponent<DialogProps> = React.forwardRef((props, ref) => {
const state = useDialog_unstable(props, ref);
export const Dialog: React.FC<DialogProps> = React.memo(props => {
const state = useDialog_unstable(props);
const contextValues = useDialogContextValues_unstable(state);

useDialogStyles_unstable(state);
return renderDialog_unstable(state);
return renderDialog_unstable(state, contextValues);
});

Dialog.displayName = 'Dialog';
Original file line number Diff line number Diff line change
@@ -1,17 +1,80 @@
import type * as React from 'react';
import type { ComponentProps, ComponentState, Slot } from '@fluentui/react-utilities';
import type { DialogContextValue } from '../../contexts/dialogContext';

export type DialogSlots = {
root: Slot<'div'>;
/**
* Dimmed background of dialog.
* The default overlay is rendered as a `<div>` with styling.
* This slot expects a `<div>` element which will replace the default overlay.
* The overlay should have `aria-hidden="true"`.
*/
overlay?: Slot<'div'>;
};

/**
* Dialog Props
*/
export type DialogProps = ComponentProps<DialogSlots>;
export type DialogOpenChangeEvent = React.KeyboardEvent | React.MouseEvent | KeyboardEvent;

/**
* State used in rendering Dialog
*/
// TODO: Add union of props to pick from DialogProps once they're implemented.
// i.e. Required<Pick<DialogProps, 'property1' | 'property2'>>;
export type DialogState = ComponentState<DialogSlots>;
export type DialogOpenChangeData =
| { type: 'escapeKeyDown'; open: boolean; event: React.KeyboardEvent }
/**
* document escape keydown defers from internal escape keydown events because of the synthetic event API
*/
| { type: 'documentEscapeKeyDown'; open: boolean; event: KeyboardEvent }
| { type: 'overlayClick'; open: boolean; event: React.MouseEvent }
| { type: 'triggerClick'; open: boolean; event: React.MouseEvent };
Comment on lines +18 to +24
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you please clarify why event is passed in data?

<Dialog onOpenChange={(event, data) => {
  // What `event` should I use?
  console.log(event, data.event)
}} />

IMO this looks redundant and confusing...


export type DialogModalType = 'modal' | 'non-modal' | 'alert';

export type DialogContextValues = {
dialog: DialogContextValue;
};

export type DialogProps = ComponentProps<Partial<DialogSlots>> & {
/**
* Dialog variations.
*
* `modal`: When this type of dialog is open, the rest of the page is dimmed out and cannot be interacted with.
* The tab sequence is kept within the dialog and moving the focus outside
* the dialog will imply closing it. This is the default type of the component.
*
* `non-modal`: When a non-modal dialog is open, the rest of the page is not dimmed out
* and users can interact with the rest of the page.
* This also implies that the tab focus can move outside the dialog when it reaches the last focusable element.
*
* `alert`: is a special type of modal dialogs that interrupts the user's workflow
* to communicate an important message or ask for a decision.
* Unlike a typical modal dialog, the user must take an action through the options given to dismiss the dialog,
* and it cannot be dismissed through the dimmed background or escape key.
*
* @default modal
*/
modalType?: DialogModalType;
/**
* Controls the open state of the dialog
* @default false
*/
open?: boolean;
/**
* Default value for the uncontrolled open state of the dialog.
* @default false
*/
defaultOpen?: boolean;
/**
* Callback fired when the component changes value from open state.
*
* @param event - a React's Synthetic event or a KeyboardEvent in case of `documentEscapeKeyDown`
* @param data - A data object with relevant information, such as open value and type
*/
onOpenChange?: (event: DialogOpenChangeEvent, data: DialogOpenChangeData) => void;
/**
* Can contain two children including {@link DialogTrigger} and {@link DialogContent}.
* Alternatively can only contain {@link DialogContent} if using trigger outside dialog, or controlling state.
*/
children: [JSX.Element, JSX.Element] | JSX.Element;
};

export type DialogState = ComponentState<DialogSlots> &
DialogContextValue & {
content: React.ReactNode;
trigger: React.ReactNode;
};
Original file line number Diff line number Diff line change
@@ -1,11 +1,3 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`Dialog renders a default state 1`] = `
<div>
<div
class="fui-Dialog"
>
Default Dialog
</div>
</div>
`;
exports[`Dialog renders a default state 1`] = `<div />`;
Loading