From 4272dabc2b546c07432804517a0cc3d4b74b22af Mon Sep 17 00:00:00 2001 From: Bernardo Sunderhus Date: Wed, 15 Jun 2022 11:17:03 +0000 Subject: [PATCH 01/17] feat: implements basic dialog --- .../react-components/react-dialog/Spec.md | 4 - .../react-dialog/package.json | 4 +- .../src/components/Dialog/Dialog.test.tsx | 18 +++- .../src/components/Dialog/Dialog.tsx | 12 +-- .../src/components/Dialog/Dialog.types.ts | 92 ++++++++++++++++--- .../src/components/Dialog/DialogContext.ts | 15 +++ .../Dialog/__snapshots__/Dialog.test.tsx.snap | 8 +- .../src/components/Dialog/renderDialog.tsx | 18 +++- .../src/components/Dialog/useDialog.test.ts | 19 ++++ .../src/components/Dialog/useDialog.ts | 69 +++++++++++--- .../Dialog/useDialogContextValues.ts | 14 +++ .../src/components/Dialog/useDialogStyles.ts | 15 ++- .../stories/Dialog/DialogDefault.stories.tsx | 6 +- .../src/stories/DialogDescription.md | 1 + 14 files changed, 238 insertions(+), 57 deletions(-) create mode 100644 packages/react-components/react-dialog/src/components/Dialog/DialogContext.ts create mode 100644 packages/react-components/react-dialog/src/components/Dialog/useDialog.test.ts create mode 100644 packages/react-components/react-dialog/src/components/Dialog/useDialogContextValues.ts create mode 100644 packages/react-components/react-dialog/src/stories/DialogDescription.md diff --git a/packages/react-components/react-dialog/Spec.md b/packages/react-components/react-dialog/Spec.md index 3164c5dc923ea..2438c1d67fc96 100644 --- a/packages/react-components/react-dialog/Spec.md +++ b/packages/react-components/react-dialog/Spec.md @@ -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 `
` with styling. diff --git a/packages/react-components/react-dialog/package.json b/packages/react-components/react-dialog/package.json index 70a26e5a80d4d..09ec04c28b142 100644 --- a/packages/react-components/react-dialog/package.json +++ b/packages/react-components/react-dialog/package.json @@ -33,7 +33,9 @@ }, "dependencies": { "@griffel/react": "^1.2.0", - "@fluentui/react-utilities": "^9.0.0", + "@fluentui/react-context-selector": "9.0.0-rc.10", + "@fluentui/react-portal": "9.0.0-rc.13", + "@fluentui/react-utilities": "9.0.0-rc.10", "tslib": "^2.1.0" }, "peerDependencies": { diff --git a/packages/react-components/react-dialog/src/components/Dialog/Dialog.test.tsx b/packages/react-components/react-dialog/src/components/Dialog/Dialog.test.tsx index 1f9d7d94fa1f4..4c559e21ef637 100644 --- a/packages/react-components/react-dialog/src/components/Dialog/Dialog.test.tsx +++ b/packages/react-components/react-dialog/src/components/Dialog/Dialog.test.tsx @@ -7,13 +7,27 @@ describe('Dialog', () => { isConformant({ Component: Dialog, displayName: 'Dialog', - disabledTests: ['component-has-static-classname-exported'], + disabledTests: [ + // Menu 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', + // Menu 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(Default Dialog); + const result = render( + +
Default Dialog
+
, + ); expect(result.container).toMatchSnapshot(); }); }); diff --git a/packages/react-components/react-dialog/src/components/Dialog/Dialog.tsx b/packages/react-components/react-dialog/src/components/Dialog/Dialog.tsx index f7bda8c1396b4..3318843ba8ed3 100644 --- a/packages/react-components/react-dialog/src/components/Dialog/Dialog.tsx +++ b/packages/react-components/react-dialog/src/components/Dialog/Dialog.tsx @@ -3,16 +3,14 @@ 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. - */ -export const Dialog: ForwardRefComponent = React.forwardRef((props, ref) => { - const state = useDialog_unstable(props, ref); +export const Dialog: React.FC = 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'; 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 ba7f19111618a..720376c843e12 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 @@ -1,17 +1,85 @@ +import type * as React from 'react'; import type { ComponentProps, ComponentState, Slot } from '@fluentui/react-utilities'; +export type DialogOpenChangeEvent = React.MouseEvent | React.KeyboardEvent; + +export type DialogOpenChangeEventHandler = (event: DialogOpenChangeEvent, data: DialogOpenChangeData) => void; + +export type DialogOpenChangeData = { + /** + * The event source of the callback invocation + */ + type: 'escapeKeyDown' | 'overlayClick' | 'triggerClick'; + /** + * The next value for the internal state of the dialog + */ + open: boolean; +}; + export type DialogSlots = { - root: Slot<'div'>; + /** + * Dimmed background of dialog. + * The default overlay is rendered as a `
` with styling. + * This slot expects a `
` element which will replace the default overlay. + * The overlay should have `aria-hidden="true"`. + */ + overlay: Slot<'div'>; +}; + +export type DialogType = 'modal' | 'non-modal' | 'alert'; + +export type DialogContextValue = { + type: DialogType; + open: boolean; + requestOpenChange: DialogOpenChangeEventHandler; +}; + +export type DialogContextValues = { + dialog: DialogContextValue; +}; + +export type DialogProps = ComponentProps> & { + /** + * 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. + */ + type?: DialogType; + /** + * Controls the open state of the dialog + * @default undefined + */ + 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. + * @default undefined + */ + onOpenChange?: DialogOpenChangeEventHandler; + /** + * 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; }; -/** - * Dialog Props - */ -export type DialogProps = ComponentProps; - -/** - * State used in rendering Dialog - */ -// TODO: Add union of props to pick from DialogProps once they're implemented. -// i.e. Required>; -export type DialogState = ComponentState; +export type DialogState = ComponentState & + DialogContextValue & { + content: React.ReactNode; + trigger: React.ReactNode; + }; diff --git a/packages/react-components/react-dialog/src/components/Dialog/DialogContext.ts b/packages/react-components/react-dialog/src/components/Dialog/DialogContext.ts new file mode 100644 index 0000000000000..970a7e94f546c --- /dev/null +++ b/packages/react-components/react-dialog/src/components/Dialog/DialogContext.ts @@ -0,0 +1,15 @@ +import { createContext, ContextSelector, useContextSelector } from '@fluentui/react-context-selector'; +import type { Context } from '@fluentui/react-context-selector'; +import type { DialogContextValue } from './Dialog.types'; + +export const DialogContext: Context = createContext({ + open: false, + type: 'modal', + requestOpenChange() { + /* noop */ + }, +}); + +export const DialogProvider = DialogContext.Provider; +export const useDialogContext_unstable = (selector: ContextSelector): T => + useContextSelector(DialogContext, selector); diff --git a/packages/react-components/react-dialog/src/components/Dialog/__snapshots__/Dialog.test.tsx.snap b/packages/react-components/react-dialog/src/components/Dialog/__snapshots__/Dialog.test.tsx.snap index 5483ee1e275b8..9d308e29bd3f4 100644 --- a/packages/react-components/react-dialog/src/components/Dialog/__snapshots__/Dialog.test.tsx.snap +++ b/packages/react-components/react-dialog/src/components/Dialog/__snapshots__/Dialog.test.tsx.snap @@ -2,10 +2,8 @@ exports[`Dialog renders a default state 1`] = `
-
- Default Dialog -
+
`; diff --git a/packages/react-components/react-dialog/src/components/Dialog/renderDialog.tsx b/packages/react-components/react-dialog/src/components/Dialog/renderDialog.tsx index eaf76b9f803e6..4726412aa86d9 100644 --- a/packages/react-components/react-dialog/src/components/Dialog/renderDialog.tsx +++ b/packages/react-components/react-dialog/src/components/Dialog/renderDialog.tsx @@ -1,13 +1,23 @@ import * as React from 'react'; import { getSlots } from '@fluentui/react-utilities'; -import type { DialogState, DialogSlots } from './Dialog.types'; +import { Portal } from '@fluentui/react-portal'; +import type { DialogState, DialogSlots, DialogContextValues } from './Dialog.types'; +import { DialogProvider } from './DialogContext'; /** * Render the final JSX of Dialog */ -export const renderDialog_unstable = (state: DialogState) => { +export const renderDialog_unstable = (state: DialogState, contextValues: DialogContextValues) => { + const { content, trigger } = state; const { slots, slotProps } = getSlots(state); - // TODO Add additional slots in the appropriate place - return ; + return ( + + {trigger} + + + {content} + + + ); }; diff --git a/packages/react-components/react-dialog/src/components/Dialog/useDialog.test.ts b/packages/react-components/react-dialog/src/components/Dialog/useDialog.test.ts new file mode 100644 index 0000000000000..12be89f12c900 --- /dev/null +++ b/packages/react-components/react-dialog/src/components/Dialog/useDialog.test.ts @@ -0,0 +1,19 @@ +import { act, renderHook } from '@testing-library/react-hooks'; +import * as React from 'react'; +import { DialogOpenChangeEvent } from './Dialog.types'; + +import { useDialog_unstable } from './useDialog'; + +describe('useAccordion_unstable', () => { + it('handle open behavior', () => { + const { result } = renderHook(() => + useDialog_unstable({ children: [React.createElement('div'), React.createElement('div')] }), + ); + + expect(result.current.open).toEqual(false); + const fakeEvent = { defaultPrevented: false } as DialogOpenChangeEvent; + act(() => result.current.requestOpenChange(fakeEvent, { open: true, type: 'triggerClick' })); + + expect(result.current.open).toEqual(true); + }); +}); 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 a476cae8dccbb..a48240a744bff 100644 --- a/packages/react-components/react-dialog/src/components/Dialog/useDialog.ts +++ b/packages/react-components/react-dialog/src/components/Dialog/useDialog.ts @@ -1,6 +1,6 @@ import * as React from 'react'; -import { getNativeElementProps } from '@fluentui/react-utilities'; -import type { DialogProps, DialogState } from './Dialog.types'; +import { resolveShorthand, useControllableState, useEventCallback } from '@fluentui/react-utilities'; +import type { DialogOpenChangeData, DialogOpenChangeEvent, DialogProps, DialogState } from './Dialog.types'; /** * Create the state required to render Dialog. @@ -9,20 +9,65 @@ import type { DialogProps, DialogState } from './Dialog.types'; * before being passed to renderDialog_unstable. * * @param props - props from this instance of Dialog - * @param ref - reference to root HTMLElement of Dialog */ -export const useDialog_unstable = (props: DialogProps, ref: React.Ref): DialogState => { +export const useDialog_unstable = (props: DialogProps): DialogState => { + const { children, open, defaultOpen, overlay, type = 'modal', onOpenChange } = props; + const [trigger, content] = childrenToTriggerAndContent(children); + + const [openValue, setOpenValue] = useControllableState({ + state: open, + defaultState: defaultOpen, + initialState: false, + }); + + const requestOpenChange = useEventCallback((event: DialogOpenChangeEvent, data: DialogOpenChangeData) => { + onOpenChange?.(event, data); + // if user prevents default then do not change state value + if (!event.defaultPrevented) { + setOpenValue(data.open); + } + }); + return { - // TODO add appropriate props/defaults components: { - // TODO add slot types here if needed (div is the default) - root: 'div', + overlay: 'div', }, - // TODO add appropriate slots, for example: - // mySlot: resolveShorthand(props.mySlot), - root: getNativeElementProps('div', { - ref, - ...props, + overlay: resolveShorthand(overlay, { + required: type === 'modal', }), + content, + trigger, + open: openValue, + requestOpenChange, + type: type, }; }; + +/** + * Extracts trigger and content from children + */ +function childrenToTriggerAndContent( + children: React.ReactNode, +): readonly [trigger: React.ReactNode, content: React.ReactNode] { + const childrenArray = React.Children.toArray(children) as React.ReactElement[]; + if (process.env.NODE_ENV !== 'production') { + if (childrenArray.length !== 1 && childrenArray.length !== 2) { + // eslint-disable-next-line no-console + console.warn( + 'Dialog must contain at least one child ,\n' + + 'and at most two children (in this order)', + ); + } + } + switch (childrenArray.length) { + // case where there's a trigger followed by content + case 2: + return [childrenArray[0], childrenArray[1]]; + // case where there's only content + case 1: + return [undefined, childrenArray[1]]; + // unknown case + default: + return [undefined, undefined]; + } +} diff --git a/packages/react-components/react-dialog/src/components/Dialog/useDialogContextValues.ts b/packages/react-components/react-dialog/src/components/Dialog/useDialogContextValues.ts new file mode 100644 index 0000000000000..c495a04c422ec --- /dev/null +++ b/packages/react-components/react-dialog/src/components/Dialog/useDialogContextValues.ts @@ -0,0 +1,14 @@ +import type { DialogContextValue, DialogContextValues, DialogState } from './Dialog.types'; + +export function useDialogContextValues_unstable(state: DialogState): DialogContextValues { + const { type, open, requestOpenChange } = state; + + // This context is created with "@fluentui/react-context-selector", these is no sense to memoize it + const dialog: DialogContextValue = { + type, + open, + requestOpenChange, + }; + + return { dialog }; +} diff --git a/packages/react-components/react-dialog/src/components/Dialog/useDialogStyles.ts b/packages/react-components/react-dialog/src/components/Dialog/useDialogStyles.ts index 15b3ef6765b1f..3f87ac42f5d01 100644 --- a/packages/react-components/react-dialog/src/components/Dialog/useDialogStyles.ts +++ b/packages/react-components/react-dialog/src/components/Dialog/useDialogStyles.ts @@ -3,17 +3,14 @@ import { makeStyles, mergeClasses } from '@griffel/react'; import type { DialogSlots, DialogState } from './Dialog.types'; export const dialogClassNames: SlotClassNames = { - root: 'fui-Dialog', + overlay: 'fui-Dialog__overlay', }; /** * Styles for the root slot */ const useStyles = makeStyles({ - root: { - // TODO Add default styles for the root element - }, - - // TODO add additional classes for different states and/or slots + root: {}, + overlay: {}, }); /** @@ -21,10 +18,10 @@ const useStyles = makeStyles({ */ export const useDialogStyles_unstable = (state: DialogState): DialogState => { const styles = useStyles(); - state.root.className = mergeClasses(dialogClassNames.root, styles.root, state.root.className); - // TODO Add class names to slots, for example: - // state.mySlot.className = mergeClasses(styles.mySlot, state.mySlot.className); + if (state.overlay) { + state.overlay.className = mergeClasses(dialogClassNames.overlay, styles.overlay, state.overlay.className); + } return state; }; diff --git a/packages/react-components/react-dialog/src/stories/Dialog/DialogDefault.stories.tsx b/packages/react-components/react-dialog/src/stories/Dialog/DialogDefault.stories.tsx index e98f4e75232f4..651588972565b 100644 --- a/packages/react-components/react-dialog/src/stories/Dialog/DialogDefault.stories.tsx +++ b/packages/react-components/react-dialog/src/stories/Dialog/DialogDefault.stories.tsx @@ -2,4 +2,8 @@ import * as React from 'react'; import { Dialog } from '@fluentui/react-dialog'; import type { DialogProps } from '@fluentui/react-dialog'; -export const Default = (props: Partial) => ; +export const Default = (props: Partial) => ( + +
+
+); diff --git a/packages/react-components/react-dialog/src/stories/DialogDescription.md b/packages/react-components/react-dialog/src/stories/DialogDescription.md new file mode 100644 index 0000000000000..041ae3715e4f9 --- /dev/null +++ b/packages/react-components/react-dialog/src/stories/DialogDescription.md @@ -0,0 +1 @@ +`Dialog` is a window overlaid on either the primary window or another dialog window. Windows under a modal dialog are inert. That is, users cannot interact with content outside an active dialog window. Inert content outside an active dialog is typically visually obscured or dimmed so it is difficult to discern, and in some implementations, attempts to interact with the inert content cause the dialog to close. From 599706efb30a5ea5a4e120c9853ae60fb5ff5895 Mon Sep 17 00:00:00 2001 From: Bernardo Sunderhus Date: Thu, 16 Jun 2022 07:26:10 +0000 Subject: [PATCH 02/17] chore: implements DialogTrigger and scaffolds DialogContent --- .../config/api-extractor.local.json | 2 +- .../react-dialog/etc/react-dialog.api.md | 82 +++++++- .../react-dialog/package.json | 2 +- .../react-dialog/src/DialogContent.ts | 1 + .../react-dialog/src/DialogTrigger.ts | 1 + .../src/components/Dialog/Dialog.types.ts | 33 ++- .../src/components/Dialog/DialogContext.ts | 15 -- .../src/components/Dialog/renderDialog.tsx | 2 +- .../src/components/Dialog/useDialog.ts | 21 +- .../Dialog/useDialogContextValues.ts | 3 +- .../DialogContent/DialogContent.test.tsx | 18 ++ .../DialogContent/DialogContent.tsx | 18 ++ .../DialogContent/DialogContent.types.ts | 17 ++ .../src/components/DialogContent/index.ts | 5 + .../DialogContent/renderDialogContent.tsx | 13 ++ .../DialogContent/useDialogContent.ts | 31 +++ .../DialogContent/useDialogContentStyles.ts | 34 +++ .../DialogTrigger/DialogTrigger.test.tsx | 198 ++++++++++++++++++ .../DialogTrigger/DialogTrigger.tsx | 18 ++ .../DialogTrigger/DialogTrigger.types.ts | 30 +++ .../__snapshots__/MenuTrigger.test.tsx.snap | 16 ++ .../src/components/DialogTrigger/index.ts | 4 + .../DialogTrigger/renderDialogTrigger.tsx | 8 + .../DialogTrigger/useDialogTrigger.ts | 45 ++++ .../src/contexts/dialogContext.ts | 42 ++++ .../react-dialog/src/index.ts | 18 ++ 26 files changed, 624 insertions(+), 53 deletions(-) create mode 100644 packages/react-components/react-dialog/src/DialogContent.ts create mode 100644 packages/react-components/react-dialog/src/DialogTrigger.ts delete mode 100644 packages/react-components/react-dialog/src/components/Dialog/DialogContext.ts create mode 100644 packages/react-components/react-dialog/src/components/DialogContent/DialogContent.test.tsx create mode 100644 packages/react-components/react-dialog/src/components/DialogContent/DialogContent.tsx create mode 100644 packages/react-components/react-dialog/src/components/DialogContent/DialogContent.types.ts create mode 100644 packages/react-components/react-dialog/src/components/DialogContent/index.ts create mode 100644 packages/react-components/react-dialog/src/components/DialogContent/renderDialogContent.tsx create mode 100644 packages/react-components/react-dialog/src/components/DialogContent/useDialogContent.ts create mode 100644 packages/react-components/react-dialog/src/components/DialogContent/useDialogContentStyles.ts create mode 100644 packages/react-components/react-dialog/src/components/DialogTrigger/DialogTrigger.test.tsx create mode 100644 packages/react-components/react-dialog/src/components/DialogTrigger/DialogTrigger.tsx create mode 100644 packages/react-components/react-dialog/src/components/DialogTrigger/DialogTrigger.types.ts create mode 100644 packages/react-components/react-dialog/src/components/DialogTrigger/__snapshots__/MenuTrigger.test.tsx.snap create mode 100644 packages/react-components/react-dialog/src/components/DialogTrigger/index.ts create mode 100644 packages/react-components/react-dialog/src/components/DialogTrigger/renderDialogTrigger.tsx create mode 100644 packages/react-components/react-dialog/src/components/DialogTrigger/useDialogTrigger.ts create mode 100644 packages/react-components/react-dialog/src/contexts/dialogContext.ts diff --git a/packages/react-components/react-dialog/config/api-extractor.local.json b/packages/react-components/react-dialog/config/api-extractor.local.json index f8d7afe5e2cd1..69e764bce3a59 100644 --- a/packages/react-components/react-dialog/config/api-extractor.local.json +++ b/packages/react-components/react-dialog/config/api-extractor.local.json @@ -1,5 +1,5 @@ { "$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json", "extends": "./api-extractor.json", - "mainEntryPointFilePath": "/dist/packages/react-components//src/index.d.ts" + "mainEntryPointFilePath": "/dist/types/packages/react-components//src/index.d.ts" } 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 b2fb41fd1dc44..d592f7180a8eb 100644 --- a/packages/react-components/react-dialog/etc/react-dialog.api.md +++ b/packages/react-components/react-dialog/etc/react-dialog.api.md @@ -6,37 +6,105 @@ import type { ComponentProps } from '@fluentui/react-utilities'; import type { ComponentState } from '@fluentui/react-utilities'; +import type { FluentTriggerComponent } from '@fluentui/react-utilities'; import type { ForwardRefComponent } from '@fluentui/react-utilities'; +import { JSXElementConstructor } from 'react'; import * as React_2 from 'react'; +import { ReactElement } from 'react'; import type { Slot } from '@fluentui/react-utilities'; import type { SlotClassNames } from '@fluentui/react-utilities'; -// @public -export const Dialog: ForwardRefComponent; +// @public (undocumented) +export const Dialog: React_2.FC; // @public (undocumented) export const dialogClassNames: SlotClassNames; // @public -export type DialogProps = ComponentProps; +export const DialogContent: ForwardRefComponent; // @public (undocumented) -export type DialogSlots = { +export const dialogContentClassNames: SlotClassNames; + +// @public +export type DialogContentProps = ComponentProps & {}; + +// @public (undocumented) +export type DialogContentSlots = { root: Slot<'div'>; }; // @public -export type DialogState = ComponentState; +export type DialogContentState = ComponentState; + +// @public (undocumented) +export type DialogProps = ComponentProps> & { + type?: DialogType; + open?: boolean; + defaultOpen?: boolean; + onOpenChange?: DialogOpenChangeEventListener; + children: [JSX.Element, JSX.Element] | JSX.Element; +}; + +// @public (undocumented) +export type DialogSlots = { + overlay: Slot<'div'>; +}; + +// @public (undocumented) +export type DialogState = ComponentState & DialogContextValue & { + content: React_2.ReactNode; + trigger: React_2.ReactNode; +}; + +// @public +export const DialogTrigger: React_2.FC & FluentTriggerComponent; + +// @public +export type DialogTriggerChildProps = Required, 'onClick' | 'aria-haspopup'>> & { + ref?: React_2.Ref; +}; + +// @public (undocumented) +export type DialogTriggerProps = { + type?: DialogTriggerType; + children: (React_2.ReactElement & { + ref?: React_2.Ref; + }) | ((props: DialogTriggerChildProps) => React_2.ReactElement | null); +}; + +// @public (undocumented) +export type DialogTriggerState = { + children: React_2.ReactElement | null; +}; + +// @public (undocumented) +export type DialogTriggerType = 'open' | 'close' | 'toggle'; + +// @public +export const renderDialog_unstable: (state: DialogState, contextValues: DialogContextValues) => JSX.Element; + +// @public +export const renderDialogContent_unstable: (state: DialogContentState) => JSX.Element; + +// @public +export const renderDialogTrigger_unstable: (state: DialogTriggerState) => ReactElement> | null; + +// @public +export const useDialog_unstable: (props: DialogProps) => DialogState; // @public -export const renderDialog_unstable: (state: DialogState) => JSX.Element; +export const useDialogContent_unstable: (props: DialogContentProps, ref: React_2.Ref) => DialogContentState; // @public -export const useDialog_unstable: (props: DialogProps, ref: React_2.Ref) => DialogState; +export const useDialogContentStyles_unstable: (state: DialogContentState) => DialogContentState; // @public export const useDialogStyles_unstable: (state: DialogState) => DialogState; +// @public +export const useDialogTrigger_unstable: (props: DialogTriggerProps) => DialogTriggerState; + // (No @packageDocumentation comment for this package) ``` diff --git a/packages/react-components/react-dialog/package.json b/packages/react-components/react-dialog/package.json index 09ec04c28b142..db6e95d1a1e66 100644 --- a/packages/react-components/react-dialog/package.json +++ b/packages/react-components/react-dialog/package.json @@ -21,7 +21,7 @@ "start": "yarn storybook", "test": "jest --passWithNoTests", "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" }, diff --git a/packages/react-components/react-dialog/src/DialogContent.ts b/packages/react-components/react-dialog/src/DialogContent.ts new file mode 100644 index 0000000000000..e3c3fa077442b --- /dev/null +++ b/packages/react-components/react-dialog/src/DialogContent.ts @@ -0,0 +1 @@ +export * from './components/DialogContent/index'; diff --git a/packages/react-components/react-dialog/src/DialogTrigger.ts b/packages/react-components/react-dialog/src/DialogTrigger.ts new file mode 100644 index 0000000000000..593f5f4186cad --- /dev/null +++ b/packages/react-components/react-dialog/src/DialogTrigger.ts @@ -0,0 +1 @@ +export * from './components/DialogTrigger/index'; 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 720376c843e12..37e015b6ebef1 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 @@ -1,20 +1,6 @@ import type * as React from 'react'; import type { ComponentProps, ComponentState, Slot } from '@fluentui/react-utilities'; - -export type DialogOpenChangeEvent = React.MouseEvent | React.KeyboardEvent; - -export type DialogOpenChangeEventHandler = (event: DialogOpenChangeEvent, data: DialogOpenChangeData) => void; - -export type DialogOpenChangeData = { - /** - * The event source of the callback invocation - */ - type: 'escapeKeyDown' | 'overlayClick' | 'triggerClick'; - /** - * The next value for the internal state of the dialog - */ - open: boolean; -}; +import type { DialogContextValue, DialogRequestOpenChangeSourceType } from '../../contexts/dialogContext'; export type DialogSlots = { /** @@ -26,14 +12,19 @@ export type DialogSlots = { overlay: Slot<'div'>; }; -export type DialogType = 'modal' | 'non-modal' | 'alert'; - -export type DialogContextValue = { - type: DialogType; +export type DialogOpenChangeEvent = React.MouseEvent | React.KeyboardEvent; +export type DialogOpenChangeData = { + /** + * The next value of open state + */ open: boolean; - requestOpenChange: DialogOpenChangeEventHandler; + type: DialogRequestOpenChangeSourceType; }; +export type DialogOpenChangeEventListener = (event: DialogOpenChangeEvent, data: DialogOpenChangeData) => void; + +export type DialogType = 'modal' | 'non-modal' | 'alert'; + export type DialogContextValues = { dialog: DialogContextValue; }; @@ -70,7 +61,7 @@ export type DialogProps = ComponentProps> & { * Callback fired when the component changes value from open state. * @default undefined */ - onOpenChange?: DialogOpenChangeEventHandler; + onOpenChange?: DialogOpenChangeEventListener; /** * Can contain two children including {@link DialogTrigger} and {@link DialogContent}. * Alternatively can only contain {@link DialogContent} if using trigger outside dialog, or controlling state. diff --git a/packages/react-components/react-dialog/src/components/Dialog/DialogContext.ts b/packages/react-components/react-dialog/src/components/Dialog/DialogContext.ts deleted file mode 100644 index 970a7e94f546c..0000000000000 --- a/packages/react-components/react-dialog/src/components/Dialog/DialogContext.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { createContext, ContextSelector, useContextSelector } from '@fluentui/react-context-selector'; -import type { Context } from '@fluentui/react-context-selector'; -import type { DialogContextValue } from './Dialog.types'; - -export const DialogContext: Context = createContext({ - open: false, - type: 'modal', - requestOpenChange() { - /* noop */ - }, -}); - -export const DialogProvider = DialogContext.Provider; -export const useDialogContext_unstable = (selector: ContextSelector): T => - useContextSelector(DialogContext, selector); diff --git a/packages/react-components/react-dialog/src/components/Dialog/renderDialog.tsx b/packages/react-components/react-dialog/src/components/Dialog/renderDialog.tsx index 4726412aa86d9..583e23d9200bd 100644 --- a/packages/react-components/react-dialog/src/components/Dialog/renderDialog.tsx +++ b/packages/react-components/react-dialog/src/components/Dialog/renderDialog.tsx @@ -2,7 +2,7 @@ import * as React from 'react'; import { getSlots } from '@fluentui/react-utilities'; import { Portal } from '@fluentui/react-portal'; import type { DialogState, DialogSlots, DialogContextValues } from './Dialog.types'; -import { DialogProvider } from './DialogContext'; +import { DialogProvider } from '../../contexts/dialogContext'; /** * Render the final JSX of Dialog 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 a48240a744bff..1b0f52c58005a 100644 --- a/packages/react-components/react-dialog/src/components/Dialog/useDialog.ts +++ b/packages/react-components/react-dialog/src/components/Dialog/useDialog.ts @@ -1,6 +1,7 @@ import * as React from 'react'; import { resolveShorthand, useControllableState, useEventCallback } from '@fluentui/react-utilities'; -import type { DialogOpenChangeData, DialogOpenChangeEvent, DialogProps, DialogState } from './Dialog.types'; +import type { DialogOpenChangeEvent, DialogProps, DialogState } from './Dialog.types'; +import { DialogRequestOpenChangeData } from '../../contexts/dialogContext'; /** * Create the state required to render Dialog. @@ -20,11 +21,12 @@ export const useDialog_unstable = (props: DialogProps): DialogState => { initialState: false, }); - const requestOpenChange = useEventCallback((event: DialogOpenChangeEvent, data: DialogOpenChangeData) => { - onOpenChange?.(event, data); + const requestOpenChange = useEventCallback((data: DialogRequestOpenChangeData) => { + const nextOpen = normalizeSetOpen(data.open)(openValue); + onOpenChange?.(data.event as DialogOpenChangeEvent, { open: nextOpen, type: data.type }); // if user prevents default then do not change state value - if (!event.defaultPrevented) { - setOpenValue(data.open); + if (!data.event.defaultPrevented) { + setOpenValue(nextOpen); } }); @@ -62,7 +64,7 @@ function childrenToTriggerAndContent( switch (childrenArray.length) { // case where there's a trigger followed by content case 2: - return [childrenArray[0], childrenArray[1]]; + return childrenArray as [trigger: React.ReactNode, content: React.ReactNode]; // case where there's only content case 1: return [undefined, childrenArray[1]]; @@ -71,3 +73,10 @@ function childrenToTriggerAndContent( return [undefined, undefined]; } } + +/** + * Normalizes a state action into a function + */ +function normalizeSetOpen(setOpen: React.SetStateAction) { + return typeof setOpen === 'function' ? setOpen : () => setOpen; +} 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 c495a04c422ec..0a1983af57156 100644 --- a/packages/react-components/react-dialog/src/components/Dialog/useDialogContextValues.ts +++ b/packages/react-components/react-dialog/src/components/Dialog/useDialogContextValues.ts @@ -1,4 +1,5 @@ -import type { DialogContextValue, DialogContextValues, DialogState } from './Dialog.types'; +import { DialogContextValue } from '../../contexts/dialogContext'; +import type { DialogContextValues, DialogState } from './Dialog.types'; export function useDialogContextValues_unstable(state: DialogState): DialogContextValues { const { type, open, requestOpenChange } = state; diff --git a/packages/react-components/react-dialog/src/components/DialogContent/DialogContent.test.tsx b/packages/react-components/react-dialog/src/components/DialogContent/DialogContent.test.tsx new file mode 100644 index 0000000000000..6ebbb8f1efd35 --- /dev/null +++ b/packages/react-components/react-dialog/src/components/DialogContent/DialogContent.test.tsx @@ -0,0 +1,18 @@ +import * as React from 'react'; +import { render } from '@testing-library/react'; +import { DialogContent } from './DialogContent'; +import { isConformant } from '../../common/isConformant'; + +describe('DialogContent', () => { + isConformant({ + Component: DialogContent, + displayName: 'DialogContent', + }); + + // TODO add more tests here, and create visual regression tests in /apps/vr-tests + + it('renders a default state', () => { + const result = render(Default DialogContent); + expect(result.container).toMatchSnapshot(); + }); +}); diff --git a/packages/react-components/react-dialog/src/components/DialogContent/DialogContent.tsx b/packages/react-components/react-dialog/src/components/DialogContent/DialogContent.tsx new file mode 100644 index 0000000000000..59eb12958e28d --- /dev/null +++ b/packages/react-components/react-dialog/src/components/DialogContent/DialogContent.tsx @@ -0,0 +1,18 @@ +import * as React from 'react'; +import { useDialogContent_unstable } from './useDialogContent'; +import { renderDialogContent_unstable } from './renderDialogContent'; +import { useDialogContentStyles_unstable } from './useDialogContentStyles'; +import type { DialogContentProps } from './DialogContent.types'; +import type { ForwardRefComponent } from '@fluentui/react-utilities'; + +/** + * DialogContent component - TODO: add more docs + */ +export const DialogContent: ForwardRefComponent = React.forwardRef((props, ref) => { + const state = useDialogContent_unstable(props, ref); + + useDialogContentStyles_unstable(state); + return renderDialogContent_unstable(state); +}); + +DialogContent.displayName = 'DialogContent'; diff --git a/packages/react-components/react-dialog/src/components/DialogContent/DialogContent.types.ts b/packages/react-components/react-dialog/src/components/DialogContent/DialogContent.types.ts new file mode 100644 index 0000000000000..d474b0cd1f9d1 --- /dev/null +++ b/packages/react-components/react-dialog/src/components/DialogContent/DialogContent.types.ts @@ -0,0 +1,17 @@ +import type { ComponentProps, ComponentState, Slot } from '@fluentui/react-utilities'; + +export type DialogContentSlots = { + root: Slot<'div'>; +}; + +/** + * DialogContent Props + */ +export type DialogContentProps = ComponentProps & {}; + +/** + * State used in rendering DialogContent + */ +export type DialogContentState = ComponentState; +// TODO: Remove semicolon from previous line, uncomment next line, and provide union of props to pick from DialogContentProps. +// & Required> diff --git a/packages/react-components/react-dialog/src/components/DialogContent/index.ts b/packages/react-components/react-dialog/src/components/DialogContent/index.ts new file mode 100644 index 0000000000000..ccedb8165f7d7 --- /dev/null +++ b/packages/react-components/react-dialog/src/components/DialogContent/index.ts @@ -0,0 +1,5 @@ +export * from './DialogContent'; +export * from './DialogContent.types'; +export * from './renderDialogContent'; +export * from './useDialogContent'; +export * from './useDialogContentStyles'; diff --git a/packages/react-components/react-dialog/src/components/DialogContent/renderDialogContent.tsx b/packages/react-components/react-dialog/src/components/DialogContent/renderDialogContent.tsx new file mode 100644 index 0000000000000..499afa482f8b7 --- /dev/null +++ b/packages/react-components/react-dialog/src/components/DialogContent/renderDialogContent.tsx @@ -0,0 +1,13 @@ +import * as React from 'react'; +import { getSlots } from '@fluentui/react-utilities'; +import type { DialogContentState, DialogContentSlots } from './DialogContent.types'; + +/** + * Render the final JSX of DialogContent + */ +export const renderDialogContent_unstable = (state: DialogContentState) => { + const { slots, slotProps } = getSlots(state); + + // TODO Add additional slots in the appropriate place + return ; +}; diff --git a/packages/react-components/react-dialog/src/components/DialogContent/useDialogContent.ts b/packages/react-components/react-dialog/src/components/DialogContent/useDialogContent.ts new file mode 100644 index 0000000000000..0f9b311bb4fc3 --- /dev/null +++ b/packages/react-components/react-dialog/src/components/DialogContent/useDialogContent.ts @@ -0,0 +1,31 @@ +import * as React from 'react'; +import { getNativeElementProps } from '@fluentui/react-utilities'; +import type { DialogContentProps, DialogContentState } from './DialogContent.types'; + +/** + * Create the state required to render DialogContent. + * + * The returned state can be modified with hooks such as useDialogContentStyles_unstable, + * before being passed to renderDialogContent_unstable. + * + * @param props - props from this instance of DialogContent + * @param ref - reference to root HTMLElement of DialogContent + */ +export const useDialogContent_unstable = ( + props: DialogContentProps, + ref: React.Ref, +): DialogContentState => { + return { + // TODO add appropriate props/defaults + components: { + // TODO add each slot's element type or component + root: 'div', + }, + // TODO add appropriate slots, for example: + // mySlot: resolveShorthand(props.mySlot), + root: getNativeElementProps('div', { + ref, + ...props, + }), + }; +}; diff --git a/packages/react-components/react-dialog/src/components/DialogContent/useDialogContentStyles.ts b/packages/react-components/react-dialog/src/components/DialogContent/useDialogContentStyles.ts new file mode 100644 index 0000000000000..0bccddafd042c --- /dev/null +++ b/packages/react-components/react-dialog/src/components/DialogContent/useDialogContentStyles.ts @@ -0,0 +1,34 @@ +import { makeStyles, mergeClasses } from '@griffel/react'; +import type { DialogContentSlots, DialogContentState } from './DialogContent.types'; +import type { SlotClassNames } from '@fluentui/react-utilities'; + +export const dialogContentClassName = 'fui-DialogContent'; +export const dialogContentClassNames: SlotClassNames = { + root: 'fui-DialogContent', + // TODO: add class names for all slots on DialogContentSlots. + // Should be of the form `: 'fui-DialogContent__` +}; + +/** + * Styles for the root slot + */ +const useStyles = makeStyles({ + root: { + // TODO Add default styles for the root element + }, + + // TODO add additional classes for different states and/or slots +}); + +/** + * Apply styling to the DialogContent slots based on the state + */ +export const useDialogContentStyles_unstable = (state: DialogContentState): DialogContentState => { + const styles = useStyles(); + state.root.className = mergeClasses(dialogContentClassName, styles.root, state.root.className); + + // TODO Add class names to slots, for example: + // state.mySlot.className = mergeClasses(styles.mySlot, state.mySlot.className); + + return state; +}; diff --git a/packages/react-components/react-dialog/src/components/DialogTrigger/DialogTrigger.test.tsx b/packages/react-components/react-dialog/src/components/DialogTrigger/DialogTrigger.test.tsx new file mode 100644 index 0000000000000..ce6c16aeec2e3 --- /dev/null +++ b/packages/react-components/react-dialog/src/components/DialogTrigger/DialogTrigger.test.tsx @@ -0,0 +1,198 @@ +import * as React from 'react'; +import { DialogTrigger } from './DialogTrigger'; +import * as renderer from 'react-test-renderer'; +import { createEvent, fireEvent, render } from '@testing-library/react'; +import { isConformant } from '../../common/isConformant'; +import { mockUseMenuContext } from '../../common/mockUseMenuContext'; +import { useMenuTriggerContext_unstable } from '../../contexts/menuTriggerContext'; +import { Enter } from '@fluentui/keyboard-keys'; + +jest.mock('../../contexts/menuContext.ts'); + +describe('MenuTrigger', () => { + beforeEach(() => mockUseMenuContext()); + + isConformant({ + disabledTests: [ + // MenuTrigger 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', + // MenuTrigger does not have own styles + 'make-styles-overrides-win', + ], + Component: DialogTrigger, + displayName: 'MenuTrigger', + requiredProps: { + children: , + }, + }); + + /** + * Note: see more visual regression tests for MenuTrigger in /apps/vr-tests. + */ + it('renders a default state', () => { + const component = renderer.create( + + + , + ); + const tree = component.toJSON(); + expect(tree).toMatchSnapshot(); + }); + + it('should retain original child callback ref', () => { + // Arrange + const ref = jest.fn(); + render( + + + , + ); + + // Assert + expect(ref).toHaveBeenCalledTimes(1); + expect(ref.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + , + ] + `); + }); + + it('should retain original child ref', () => { + // Arrange + const cb = jest.fn(); + const TestComponent = () => { + const ref = React.useRef(null); + React.useEffect(() => { + cb(ref.current); + }, []); + return ( + + + + ); + }; + render(); + + // Assert + expect(cb).toHaveBeenCalledTimes(1); + expect(cb.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + , + ] + `); + }); + + it('should not open menu when aria-disabled is true', () => { + const setOpen = jest.fn(); + mockUseMenuContext({ setOpen }); + + const { getByRole } = render( + + + , + ); + fireEvent.click(getByRole('button')); + + expect(setOpen).toBeCalledTimes(0); + }); + + it('should open menu when aria-disabled is false', () => { + const setOpen = jest.fn(); + mockUseMenuContext({ setOpen }); + + const { getByRole } = render( + + + , + ); + fireEvent.click(getByRole('button')); + + expect(setOpen).toBeCalledTimes(1); + expect(setOpen).toHaveBeenCalledWith(expect.anything(), expect.objectContaining({ open: true })); + }); + + it('should open menu when trigger is disabled', () => { + const setOpen = jest.fn(); + mockUseMenuContext({ setOpen }); + + const { getByRole } = render( + + + , + ); + fireEvent.click(getByRole('button')); + + expect(setOpen).toBeCalledTimes(0); + }); + + it('should set MenuTriggerContext to false if not a submenu', () => { + mockUseMenuContext({ isSubmenu: false }); + let contextValue: boolean | undefined; + const TestComponent = () => { + contextValue = useMenuTriggerContext_unstable(); + + return null; + }; + + render( + + + , + ); + + expect(contextValue).toBe(false); + }); + + it('should set MenuTriggerContext to true if in a submenu', () => { + mockUseMenuContext({ isSubmenu: true }); + let contextValue: boolean | undefined; + const TestComponent = () => { + contextValue = useMenuTriggerContext_unstable(); + + return null; + }; + + render( + + + , + ); + + expect(contextValue).toBe(true); + }); + + it('should not keyboard click when event is default prevented', () => { + const onClick = jest.fn(); + const { getByRole } = render( + +
+ trigger +
+
, + ); + const event = createEvent.keyDown(getByRole('button'), { key: Enter }); + event.preventDefault(); + fireEvent(getByRole('button'), event); + + expect(onClick).toBeCalledTimes(0); + }); +}); diff --git a/packages/react-components/react-dialog/src/components/DialogTrigger/DialogTrigger.tsx b/packages/react-components/react-dialog/src/components/DialogTrigger/DialogTrigger.tsx new file mode 100644 index 0000000000000..82b3df92c6897 --- /dev/null +++ b/packages/react-components/react-dialog/src/components/DialogTrigger/DialogTrigger.tsx @@ -0,0 +1,18 @@ +import * as React from 'react'; +import { useDialogTrigger_unstable } from './useDialogTrigger'; +import { renderDialogTrigger_unstable } from './renderDialogTrigger'; +import type { DialogTriggerProps } from './DialogTrigger.types'; +import type { FluentTriggerComponent } from '@fluentui/react-utilities'; + +/** + * Wraps a trigger element as an only child + * and adds the necessary event handling to open a popup menu + */ +export const DialogTrigger: React.FC & FluentTriggerComponent = props => { + const state = useDialogTrigger_unstable(props); + + return renderDialogTrigger_unstable(state); +}; + +DialogTrigger.displayName = 'DialogTrigger'; +DialogTrigger.isFluentTriggerComponent = true; diff --git a/packages/react-components/react-dialog/src/components/DialogTrigger/DialogTrigger.types.ts b/packages/react-components/react-dialog/src/components/DialogTrigger/DialogTrigger.types.ts new file mode 100644 index 0000000000000..942bc9d634e31 --- /dev/null +++ b/packages/react-components/react-dialog/src/components/DialogTrigger/DialogTrigger.types.ts @@ -0,0 +1,30 @@ +import * as React from 'react'; + +export type DialogTriggerType = 'open' | 'close' | 'toggle'; + +export type DialogTriggerProps = { + /** + * Explicitly declare if the trigger is responsible for opening, + * closing or toggling a Dialog visibility state. + * @default 'toggle' + */ + type?: DialogTriggerType; + /** + * Explicitly require single child or render function + * to inject properties + */ + children: + | (React.ReactElement & { ref?: React.Ref }) + | ((props: DialogTriggerChildProps) => React.ReactElement | null); +}; + +/** + * Props that are passed to the child of the DialogTrigger when cloned to ensure correct behaviour for the Dialog + */ +export type DialogTriggerChildProps = Required, 'onClick' | 'aria-haspopup'>> & { + ref?: React.Ref; +}; + +export type DialogTriggerState = { + children: React.ReactElement | null; +}; diff --git a/packages/react-components/react-dialog/src/components/DialogTrigger/__snapshots__/MenuTrigger.test.tsx.snap b/packages/react-components/react-dialog/src/components/DialogTrigger/__snapshots__/MenuTrigger.test.tsx.snap new file mode 100644 index 0000000000000..7826501c5a172 --- /dev/null +++ b/packages/react-components/react-dialog/src/components/DialogTrigger/__snapshots__/MenuTrigger.test.tsx.snap @@ -0,0 +1,16 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`MenuTrigger renders a default state 1`] = ` + +`; diff --git a/packages/react-components/react-dialog/src/components/DialogTrigger/index.ts b/packages/react-components/react-dialog/src/components/DialogTrigger/index.ts new file mode 100644 index 0000000000000..cefe8176cb56b --- /dev/null +++ b/packages/react-components/react-dialog/src/components/DialogTrigger/index.ts @@ -0,0 +1,4 @@ +export * from './DialogTrigger'; +export * from './DialogTrigger.types'; +export * from './renderDialogTrigger'; +export * from './useDialogTrigger'; diff --git a/packages/react-components/react-dialog/src/components/DialogTrigger/renderDialogTrigger.tsx b/packages/react-components/react-dialog/src/components/DialogTrigger/renderDialogTrigger.tsx new file mode 100644 index 0000000000000..6cd376795dd8c --- /dev/null +++ b/packages/react-components/react-dialog/src/components/DialogTrigger/renderDialogTrigger.tsx @@ -0,0 +1,8 @@ +import type { DialogTriggerState } from './DialogTrigger.types'; + +/** + * Render the final JSX of MenuTrigger + * + * Only renders children + */ +export const renderDialogTrigger_unstable = (state: DialogTriggerState) => state.children; diff --git a/packages/react-components/react-dialog/src/components/DialogTrigger/useDialogTrigger.ts b/packages/react-components/react-dialog/src/components/DialogTrigger/useDialogTrigger.ts new file mode 100644 index 0000000000000..fbc71af42e73c --- /dev/null +++ b/packages/react-components/react-dialog/src/components/DialogTrigger/useDialogTrigger.ts @@ -0,0 +1,45 @@ +import { applyTriggerPropsToChildren, getTriggerChild, useEventCallback } from '@fluentui/react-utilities'; +import * as React from 'react'; +import { useDialogContext_unstable } from '../../contexts/dialogContext'; +import { + DialogTriggerChildProps, + DialogTriggerProps, + DialogTriggerState, + DialogTriggerType, +} from './DialogTrigger.types'; + +/** + * Create the state required to render DialogTrigger. + * Clones the only child component and adds necessary event handling behaviours to open a popup Dialog + * + * @param props - props from this instance of DialogTrigger + */ +export const useDialogTrigger_unstable = (props: DialogTriggerProps): DialogTriggerState => { + const { children, type = 'toggle' } = props; + const child = React.isValidElement(children) ? getTriggerChild(children) : undefined; + + const requestOpenChange = useDialogContext_unstable(ctx => ctx.requestOpenChange); + + const handleClick = useEventCallback((ev: React.MouseEvent) => { + child?.props.onClick?.(ev); + requestOpenChange({ event: ev, type: 'triggerClick', open: updateOpen(type) }); + }); + + return { + children: applyTriggerPropsToChildren(children, { + 'aria-haspopup': 'dialog', + onClick: handleClick, + }), + }; +}; + +function updateOpen(type: DialogTriggerType): React.SetStateAction { + switch (type) { + case 'close': + return false; + case 'open': + return true; + case 'toggle': + return curr => !curr; + } +} diff --git a/packages/react-components/react-dialog/src/contexts/dialogContext.ts b/packages/react-components/react-dialog/src/contexts/dialogContext.ts new file mode 100644 index 0000000000000..f1693042dad63 --- /dev/null +++ b/packages/react-components/react-dialog/src/contexts/dialogContext.ts @@ -0,0 +1,42 @@ +import { createContext, ContextSelector, useContextSelector } from '@fluentui/react-context-selector'; +import type { Context } from '@fluentui/react-context-selector'; +import type { DialogType } from '../Dialog'; +import * as React from 'react'; + +export type DialogRequestOpenChangeSourceType = 'escapeKeyDown' | 'overlayClick' | 'triggerClick'; + +export type DialogRequestOpenChangeData = { + /** + * The event that originated the change + */ + event: React.SyntheticEvent; + /** + * The source type of the callback invocation + */ + type: DialogRequestOpenChangeSourceType; + /** + * The next value for the internal state of the dialog or a function to update it + */ + open: React.SetStateAction; +}; + +export type DialogContextValue = { + type: DialogType; + open: boolean; + /** + * Requests dialog main component to update it's internal open state + */ + requestOpenChange(data: DialogRequestOpenChangeData): void; +}; + +export const DialogContext: Context = createContext({ + open: false, + type: 'modal', + requestOpenChange() { + /* noop */ + }, +}); + +export const DialogProvider = DialogContext.Provider; +export const useDialogContext_unstable = (selector: ContextSelector): T => + useContextSelector(DialogContext, selector); diff --git a/packages/react-components/react-dialog/src/index.ts b/packages/react-components/react-dialog/src/index.ts index 0de05da73985b..1ae5803440ed0 100644 --- a/packages/react-components/react-dialog/src/index.ts +++ b/packages/react-components/react-dialog/src/index.ts @@ -6,3 +6,21 @@ export { useDialog_unstable, } from './Dialog'; export type { DialogProps, DialogSlots, DialogState } from './Dialog'; + +export { DialogTrigger, useDialogTrigger_unstable, renderDialogTrigger_unstable } from './DialogTrigger'; +export type { + DialogTriggerProps, + DialogTriggerChildProps, + DialogTriggerState, + DialogTriggerType, +} from './DialogTrigger'; + +export { + DialogContent, + dialogContentClassNames, + useDialogContentStyles_unstable, + useDialogContent_unstable, + renderDialogContent_unstable, +} from './DialogContent'; + +export type { DialogContentProps, DialogContentSlots, DialogContentState } from './DialogContent'; From 01bc61e7060686bf590473c825550bec25912dc0 Mon Sep 17 00:00:00 2001 From: Bernardo Sunderhus Date: Thu, 16 Jun 2022 09:13:23 +0000 Subject: [PATCH 03/17] Adds root back to Dialog --- .../src/components/Dialog/Dialog.tsx | 5 +++-- .../src/components/Dialog/Dialog.types.ts | 1 + .../src/components/Dialog/renderDialog.tsx | 14 +++++++++----- .../src/components/Dialog/useDialog.ts | 16 +++++++++++++--- .../src/components/Dialog/useDialogStyles.ts | 3 +++ .../DialogContent/useDialogContentStyles.ts | 5 +---- .../src/stories/Dialog/DialogDefault.stories.tsx | 7 +++++-- 7 files changed, 35 insertions(+), 16 deletions(-) diff --git a/packages/react-components/react-dialog/src/components/Dialog/Dialog.tsx b/packages/react-components/react-dialog/src/components/Dialog/Dialog.tsx index 3318843ba8ed3..131a351b5c8c9 100644 --- a/packages/react-components/react-dialog/src/components/Dialog/Dialog.tsx +++ b/packages/react-components/react-dialog/src/components/Dialog/Dialog.tsx @@ -4,9 +4,10 @@ import { renderDialog_unstable } from './renderDialog'; import { useDialogStyles_unstable } from './useDialogStyles'; import type { DialogProps } from './Dialog.types'; import { useDialogContextValues_unstable } from './useDialogContextValues'; +import { ForwardRefComponent } from '@fluentui/react-utilities'; -export const Dialog: React.FC = React.memo(props => { - const state = useDialog_unstable(props); +export const Dialog: ForwardRefComponent = React.forwardRef((props, ref) => { + const state = useDialog_unstable(props, ref); const contextValues = useDialogContextValues_unstable(state); useDialogStyles_unstable(state); 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 37e015b6ebef1..81d72fffb2b83 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 @@ -10,6 +10,7 @@ export type DialogSlots = { * The overlay should have `aria-hidden="true"`. */ overlay: Slot<'div'>; + root: Slot<'div'>; }; export type DialogOpenChangeEvent = React.MouseEvent | React.KeyboardEvent; diff --git a/packages/react-components/react-dialog/src/components/Dialog/renderDialog.tsx b/packages/react-components/react-dialog/src/components/Dialog/renderDialog.tsx index 583e23d9200bd..22ef19422e25f 100644 --- a/packages/react-components/react-dialog/src/components/Dialog/renderDialog.tsx +++ b/packages/react-components/react-dialog/src/components/Dialog/renderDialog.tsx @@ -8,16 +8,20 @@ import { DialogProvider } from '../../contexts/dialogContext'; * Render the final JSX of Dialog */ export const renderDialog_unstable = (state: DialogState, contextValues: DialogContextValues) => { - const { content, trigger } = state; + const { content, trigger, open } = state; const { slots, slotProps } = getSlots(state); return ( {trigger} - - - {content} - + {open && ( + + + + {content} + + + )} ); }; 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 1b0f52c58005a..bb4df368467a7 100644 --- a/packages/react-components/react-dialog/src/components/Dialog/useDialog.ts +++ b/packages/react-components/react-dialog/src/components/Dialog/useDialog.ts @@ -1,5 +1,10 @@ import * as React from 'react'; -import { resolveShorthand, useControllableState, useEventCallback } from '@fluentui/react-utilities'; +import { + getNativeElementProps, + resolveShorthand, + useControllableState, + useEventCallback, +} from '@fluentui/react-utilities'; import type { DialogOpenChangeEvent, DialogProps, DialogState } from './Dialog.types'; import { DialogRequestOpenChangeData } from '../../contexts/dialogContext'; @@ -11,8 +16,8 @@ import { DialogRequestOpenChangeData } from '../../contexts/dialogContext'; * * @param props - props from this instance of Dialog */ -export const useDialog_unstable = (props: DialogProps): DialogState => { - const { children, open, defaultOpen, overlay, type = 'modal', onOpenChange } = props; +export const useDialog_unstable = (props: DialogProps, ref: React.Ref): DialogState => { + const { children, open, defaultOpen, overlay, type = 'modal', onOpenChange, as } = props; const [trigger, content] = childrenToTriggerAndContent(children); const [openValue, setOpenValue] = useControllableState({ @@ -33,10 +38,15 @@ export const useDialog_unstable = (props: DialogProps): DialogState => { return { components: { overlay: 'div', + root: 'div', }, overlay: resolveShorthand(overlay, { required: type === 'modal', }), + root: getNativeElementProps(as ?? 'div', { + ...props, + ref, + }), content, trigger, open: openValue, diff --git a/packages/react-components/react-dialog/src/components/Dialog/useDialogStyles.ts b/packages/react-components/react-dialog/src/components/Dialog/useDialogStyles.ts index 3f87ac42f5d01..f33e09fe26e25 100644 --- a/packages/react-components/react-dialog/src/components/Dialog/useDialogStyles.ts +++ b/packages/react-components/react-dialog/src/components/Dialog/useDialogStyles.ts @@ -3,6 +3,7 @@ import { makeStyles, mergeClasses } from '@griffel/react'; import type { DialogSlots, DialogState } from './Dialog.types'; export const dialogClassNames: SlotClassNames = { + root: 'fui-Dialog', overlay: 'fui-Dialog__overlay', }; /** @@ -19,6 +20,8 @@ const useStyles = makeStyles({ export const useDialogStyles_unstable = (state: DialogState): DialogState => { const styles = useStyles(); + state.root.className = mergeClasses(dialogClassNames.root, styles.root, state.root.className); + if (state.overlay) { state.overlay.className = mergeClasses(dialogClassNames.overlay, styles.overlay, state.overlay.className); } diff --git a/packages/react-components/react-dialog/src/components/DialogContent/useDialogContentStyles.ts b/packages/react-components/react-dialog/src/components/DialogContent/useDialogContentStyles.ts index 0bccddafd042c..cd4f6ba6f50a8 100644 --- a/packages/react-components/react-dialog/src/components/DialogContent/useDialogContentStyles.ts +++ b/packages/react-components/react-dialog/src/components/DialogContent/useDialogContentStyles.ts @@ -2,11 +2,8 @@ import { makeStyles, mergeClasses } from '@griffel/react'; import type { DialogContentSlots, DialogContentState } from './DialogContent.types'; import type { SlotClassNames } from '@fluentui/react-utilities'; -export const dialogContentClassName = 'fui-DialogContent'; export const dialogContentClassNames: SlotClassNames = { root: 'fui-DialogContent', - // TODO: add class names for all slots on DialogContentSlots. - // Should be of the form `: 'fui-DialogContent__` }; /** @@ -25,7 +22,7 @@ const useStyles = makeStyles({ */ export const useDialogContentStyles_unstable = (state: DialogContentState): DialogContentState => { const styles = useStyles(); - state.root.className = mergeClasses(dialogContentClassName, styles.root, state.root.className); + state.root.className = mergeClasses(dialogContentClassNames.root, styles.root, state.root.className); // TODO Add class names to slots, for example: // state.mySlot.className = mergeClasses(styles.mySlot, state.mySlot.className); diff --git a/packages/react-components/react-dialog/src/stories/Dialog/DialogDefault.stories.tsx b/packages/react-components/react-dialog/src/stories/Dialog/DialogDefault.stories.tsx index 651588972565b..7bb120a4581ab 100644 --- a/packages/react-components/react-dialog/src/stories/Dialog/DialogDefault.stories.tsx +++ b/packages/react-components/react-dialog/src/stories/Dialog/DialogDefault.stories.tsx @@ -1,9 +1,12 @@ import * as React from 'react'; -import { Dialog } from '@fluentui/react-dialog'; +import { Dialog, DialogTrigger, DialogContent } from '@fluentui/react-dialog'; import type { DialogProps } from '@fluentui/react-dialog'; export const Default = (props: Partial) => ( -
+ + + + Dialog Content
); From 78fd8f312a785889d75af39940128a047c89eafa Mon Sep 17 00:00:00 2001 From: Bernardo Sunderhus Date: Sun, 19 Jun 2022 14:28:07 +0000 Subject: [PATCH 04/17] Scaffolds DialogTitle --- .../DialogTitle/DialogTitle.test.tsx | 18 +++++++++++++ .../components/DialogTitle/DialogTitle.tsx | 18 +++++++++++++ .../DialogTitle/DialogTitle.types.ts | 17 +++++++++++++ .../src/components/DialogTitle/index.ts | 5 ++++ .../DialogTitle/renderDialogTitle.tsx | 13 ++++++++++ .../components/DialogTitle/useDialogTitle.ts | 25 +++++++++++++++++++ .../DialogTitle/useDialogTitleStyles.ts | 23 +++++++++++++++++ 7 files changed, 119 insertions(+) create mode 100644 packages/react-components/react-dialog/src/components/DialogTitle/DialogTitle.test.tsx create mode 100644 packages/react-components/react-dialog/src/components/DialogTitle/DialogTitle.tsx create mode 100644 packages/react-components/react-dialog/src/components/DialogTitle/DialogTitle.types.ts create mode 100644 packages/react-components/react-dialog/src/components/DialogTitle/index.ts create mode 100644 packages/react-components/react-dialog/src/components/DialogTitle/renderDialogTitle.tsx create mode 100644 packages/react-components/react-dialog/src/components/DialogTitle/useDialogTitle.ts create mode 100644 packages/react-components/react-dialog/src/components/DialogTitle/useDialogTitleStyles.ts diff --git a/packages/react-components/react-dialog/src/components/DialogTitle/DialogTitle.test.tsx b/packages/react-components/react-dialog/src/components/DialogTitle/DialogTitle.test.tsx new file mode 100644 index 0000000000000..120d2b3d88947 --- /dev/null +++ b/packages/react-components/react-dialog/src/components/DialogTitle/DialogTitle.test.tsx @@ -0,0 +1,18 @@ +import * as React from 'react'; +import { render } from '@testing-library/react'; +import { DialogTitle } from './DialogTitle'; +import { isConformant } from '../../common/isConformant'; + +describe('DialogTitle', () => { + isConformant({ + Component: DialogTitle, + displayName: 'DialogTitle', + }); + + // TODO add more tests here, and create visual regression tests in /apps/vr-tests + + it('renders a default state', () => { + const result = render(Default DialogTitle); + expect(result.container).toMatchSnapshot(); + }); +}); diff --git a/packages/react-components/react-dialog/src/components/DialogTitle/DialogTitle.tsx b/packages/react-components/react-dialog/src/components/DialogTitle/DialogTitle.tsx new file mode 100644 index 0000000000000..78be98b59105c --- /dev/null +++ b/packages/react-components/react-dialog/src/components/DialogTitle/DialogTitle.tsx @@ -0,0 +1,18 @@ +import * as React from 'react'; +import { useDialogTitle_unstable } from './useDialogTitle'; +import { renderDialogTitle_unstable } from './renderDialogTitle'; +import { useDialogTitleStyles_unstable } from './useDialogTitleStyles'; +import type { DialogTitleProps } from './DialogTitle.types'; +import type { ForwardRefComponent } from '@fluentui/react-utilities'; + +/** + * DialogTitle component - TODO: add more docs + */ +export const DialogTitle: ForwardRefComponent = React.forwardRef((props, ref) => { + const state = useDialogTitle_unstable(props, ref); + + useDialogTitleStyles_unstable(state); + return renderDialogTitle_unstable(state); +}); + +DialogTitle.displayName = 'DialogTitle'; diff --git a/packages/react-components/react-dialog/src/components/DialogTitle/DialogTitle.types.ts b/packages/react-components/react-dialog/src/components/DialogTitle/DialogTitle.types.ts new file mode 100644 index 0000000000000..1c2e9f6b164cd --- /dev/null +++ b/packages/react-components/react-dialog/src/components/DialogTitle/DialogTitle.types.ts @@ -0,0 +1,17 @@ +import type { ComponentProps, ComponentState, Slot } from '@fluentui/react-utilities'; + +export type DialogTitleSlots = { + root: Slot<'div'>; +}; + +/** + * DialogTitle Props + */ +export type DialogTitleProps = ComponentProps & {}; + +/** + * State used in rendering DialogTitle + */ +export type DialogTitleState = ComponentState; +// TODO: Remove semicolon from previous line, uncomment next line, and provide union of props to pick from DialogTitleProps. +// & Required> diff --git a/packages/react-components/react-dialog/src/components/DialogTitle/index.ts b/packages/react-components/react-dialog/src/components/DialogTitle/index.ts new file mode 100644 index 0000000000000..047ee38dc730a --- /dev/null +++ b/packages/react-components/react-dialog/src/components/DialogTitle/index.ts @@ -0,0 +1,5 @@ +export * from './DialogTitle'; +export * from './DialogTitle.types'; +export * from './renderDialogTitle'; +export * from './useDialogTitle'; +export * from './useDialogTitleStyles'; diff --git a/packages/react-components/react-dialog/src/components/DialogTitle/renderDialogTitle.tsx b/packages/react-components/react-dialog/src/components/DialogTitle/renderDialogTitle.tsx new file mode 100644 index 0000000000000..2fd3a1d2a95d3 --- /dev/null +++ b/packages/react-components/react-dialog/src/components/DialogTitle/renderDialogTitle.tsx @@ -0,0 +1,13 @@ +import * as React from 'react'; +import { getSlots } from '@fluentui/react-utilities'; +import type { DialogTitleState, DialogTitleSlots } from './DialogTitle.types'; + +/** + * Render the final JSX of DialogTitle + */ +export const renderDialogTitle_unstable = (state: DialogTitleState) => { + const { slots, slotProps } = getSlots(state); + + // TODO Add additional slots in the appropriate place + return ; +}; diff --git a/packages/react-components/react-dialog/src/components/DialogTitle/useDialogTitle.ts b/packages/react-components/react-dialog/src/components/DialogTitle/useDialogTitle.ts new file mode 100644 index 0000000000000..7120b2106dfba --- /dev/null +++ b/packages/react-components/react-dialog/src/components/DialogTitle/useDialogTitle.ts @@ -0,0 +1,25 @@ +import * as React from 'react'; +import { getNativeElementProps } from '@fluentui/react-utilities'; +import type { DialogTitleProps, DialogTitleState } from './DialogTitle.types'; + +/** + * Create the state required to render DialogTitle. + * + * The returned state can be modified with hooks such as useDialogTitleStyles_unstable, + * before being passed to renderDialogTitle_unstable. + * + * @param props - props from this instance of DialogTitle + * @param ref - reference to root HTMLElement of DialogTitle + */ +export const useDialogTitle_unstable = (props: DialogTitleProps, ref: React.Ref): DialogTitleState => { + const { as = 'div' } = props; + return { + components: { + root: 'div', + }, + root: getNativeElementProps(as, { + ref, + ...props, + }), + }; +}; diff --git a/packages/react-components/react-dialog/src/components/DialogTitle/useDialogTitleStyles.ts b/packages/react-components/react-dialog/src/components/DialogTitle/useDialogTitleStyles.ts new file mode 100644 index 0000000000000..33540c9ed52d3 --- /dev/null +++ b/packages/react-components/react-dialog/src/components/DialogTitle/useDialogTitleStyles.ts @@ -0,0 +1,23 @@ +import { makeStyles, mergeClasses } from '@griffel/react'; +import type { DialogTitleSlots, DialogTitleState } from './DialogTitle.types'; +import type { SlotClassNames } from '@fluentui/react-utilities'; + +export const dialogTitleClassNames: SlotClassNames = { + root: 'fui-DialogTitle', +}; + +/** + * Styles for the root slot + */ +const useStyles = makeStyles({ + root: {}, +}); + +/** + * Apply styling to the DialogTitle slots based on the state + */ +export const useDialogTitleStyles_unstable = (state: DialogTitleState): DialogTitleState => { + const styles = useStyles(); + state.root.className = mergeClasses(dialogTitleClassNames.root, styles.root, state.root.className); + return state; +}; From 7cb7c1da3a294d7002816ad418e5c60ae6f6a20c Mon Sep 17 00:00:00 2001 From: Bernardo Sunderhus Date: Sun, 19 Jun 2022 14:28:24 +0000 Subject: [PATCH 05/17] Implements document escape listener --- .../react-components/react-dialog/Spec.md | 1 - .../react-dialog/etc/react-dialog.api.md | 38 ++++- .../react-dialog/package.json | 4 + .../react-dialog/src/DialogTitle.ts | 1 + .../src/components/Dialog/Dialog.types.ts | 18 +- .../src/components/Dialog/useDialog.test.ts | 9 +- .../src/components/Dialog/useDialog.ts | 156 +++++++++++++++--- .../Dialog/useDialogContextValues.ts | 14 +- .../src/components/Dialog/useDialogStyles.ts | 18 +- .../DialogContent/DialogContent.tsx | 3 +- .../DialogContent/DialogContent.types.ts | 4 +- .../DialogContent/renderDialogContent.tsx | 1 - .../DialogContent/useDialogContent.ts | 22 ++- .../DialogContent/useDialogContentStyles.ts | 21 ++- .../DialogTrigger/useDialogTrigger.ts | 19 ++- .../src/contexts/dialogContext.ts | 24 +-- .../react-dialog/src/index.ts | 9 + .../stories/Dialog/DialogDefault.stories.tsx | 30 +++- 18 files changed, 300 insertions(+), 92 deletions(-) create mode 100644 packages/react-components/react-dialog/src/DialogTitle.ts diff --git a/packages/react-components/react-dialog/Spec.md b/packages/react-components/react-dialog/Spec.md index 2438c1d67fc96..fe1a12c630ca5 100644 --- a/packages/react-components/react-dialog/Spec.md +++ b/packages/react-components/react-dialog/Spec.md @@ -162,7 +162,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 = { 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 d592f7180a8eb..e19683c6dc47a 100644 --- a/packages/react-components/react-dialog/etc/react-dialog.api.md +++ b/packages/react-components/react-dialog/etc/react-dialog.api.md @@ -7,15 +7,16 @@ import type { ComponentProps } from '@fluentui/react-utilities'; import type { ComponentState } from '@fluentui/react-utilities'; import type { FluentTriggerComponent } from '@fluentui/react-utilities'; -import type { ForwardRefComponent } from '@fluentui/react-utilities'; +import { ForwardRefComponent } from '@fluentui/react-utilities'; import { JSXElementConstructor } from 'react'; import * as React_2 from 'react'; import { ReactElement } from 'react'; import type { Slot } from '@fluentui/react-utilities'; import type { SlotClassNames } from '@fluentui/react-utilities'; +import { Types } from '@fluentui/react-tabster'; // @public (undocumented) -export const Dialog: React_2.FC; +export const Dialog: ForwardRefComponent; // @public (undocumented) export const dialogClassNames: SlotClassNames; @@ -27,7 +28,7 @@ export const DialogContent: ForwardRefComponent; export const dialogContentClassNames: SlotClassNames; // @public -export type DialogContentProps = ComponentProps & {}; +export type DialogContentProps = ComponentProps; // @public (undocumented) export type DialogContentSlots = { @@ -42,13 +43,14 @@ export type DialogProps = ComponentProps> & { type?: DialogType; open?: boolean; defaultOpen?: boolean; - onOpenChange?: DialogOpenChangeEventListener; + onOpenChange?(...args: DialogOpenChangeArgs): void; children: [JSX.Element, JSX.Element] | JSX.Element; }; // @public (undocumented) export type DialogSlots = { overlay: Slot<'div'>; + root: Slot<'div'>; }; // @public (undocumented) @@ -57,6 +59,23 @@ export type DialogState = ComponentState & DialogContextValue & { trigger: React_2.ReactNode; }; +// @public +export const DialogTitle: ForwardRefComponent; + +// @public (undocumented) +export const dialogTitleClassNames: SlotClassNames; + +// @public +export type DialogTitleProps = ComponentProps & {}; + +// @public (undocumented) +export type DialogTitleSlots = { + root: Slot<'div'>; +}; + +// @public +export type DialogTitleState = ComponentState; + // @public export const DialogTrigger: React_2.FC & FluentTriggerComponent; @@ -87,11 +106,14 @@ export const renderDialog_unstable: (state: DialogState, contextValues: DialogCo // @public export const renderDialogContent_unstable: (state: DialogContentState) => JSX.Element; +// @public +export const renderDialogTitle_unstable: (state: DialogTitleState) => JSX.Element; + // @public export const renderDialogTrigger_unstable: (state: DialogTriggerState) => ReactElement> | null; // @public -export const useDialog_unstable: (props: DialogProps) => DialogState; +export const useDialog_unstable: (props: DialogProps, ref: React_2.Ref) => DialogState; // @public export const useDialogContent_unstable: (props: DialogContentProps, ref: React_2.Ref) => DialogContentState; @@ -102,6 +124,12 @@ export const useDialogContentStyles_unstable: (state: DialogContentState) => Dia // @public export const useDialogStyles_unstable: (state: DialogState) => DialogState; +// @public +export const useDialogTitle_unstable: (props: DialogTitleProps, ref: React_2.Ref) => DialogTitleState; + +// @public +export const useDialogTitleStyles_unstable: (state: DialogTitleState) => DialogTitleState; + // @public export const useDialogTrigger_unstable: (props: DialogTriggerProps) => DialogTriggerState; diff --git a/packages/react-components/react-dialog/package.json b/packages/react-components/react-dialog/package.json index db6e95d1a1e66..5998f520108c9 100644 --- a/packages/react-components/react-dialog/package.json +++ b/packages/react-components/react-dialog/package.json @@ -33,7 +33,11 @@ }, "dependencies": { "@griffel/react": "^1.2.0", + "@fluentui/keyboard-keys": "9.0.0-rc.6", "@fluentui/react-context-selector": "9.0.0-rc.10", + "@fluentui/react-shared-contexts": "9.0.0-rc.10", + "@fluentui/react-tabster": "9.0.0-rc.13", + "@fluentui/react-theme": "9.0.0-rc.9", "@fluentui/react-portal": "9.0.0-rc.13", "@fluentui/react-utilities": "9.0.0-rc.10", "tslib": "^2.1.0" diff --git a/packages/react-components/react-dialog/src/DialogTitle.ts b/packages/react-components/react-dialog/src/DialogTitle.ts new file mode 100644 index 0000000000000..925cc7018a65f --- /dev/null +++ b/packages/react-components/react-dialog/src/DialogTitle.ts @@ -0,0 +1 @@ +export * from './components/DialogTitle/index'; 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 81d72fffb2b83..8c7055116591f 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 @@ -1,6 +1,6 @@ import type * as React from 'react'; import type { ComponentProps, ComponentState, Slot } from '@fluentui/react-utilities'; -import type { DialogContextValue, DialogRequestOpenChangeSourceType } from '../../contexts/dialogContext'; +import type { DialogContextValue } from '../../contexts/dialogContext'; export type DialogSlots = { /** @@ -13,16 +13,14 @@ export type DialogSlots = { root: Slot<'div'>; }; -export type DialogOpenChangeEvent = React.MouseEvent | React.KeyboardEvent; -export type DialogOpenChangeData = { +export type DialogOpenChangeArgs = + | [event: React.KeyboardEvent, data: { type: 'escapeKeyDown'; open: boolean }] /** - * The next value of open state + * document escape keydown defers from internal escape keydown events because of the synthetic event API */ - open: boolean; - type: DialogRequestOpenChangeSourceType; -}; - -export type DialogOpenChangeEventListener = (event: DialogOpenChangeEvent, data: DialogOpenChangeData) => void; + | [event: KeyboardEvent, data: { type: 'documentEscapeKeyDown'; open: boolean }] + | [event: React.MouseEvent, data: { type: 'overlayClick'; open: boolean }] + | [event: React.MouseEvent, data: { type: 'triggerClick'; open: boolean }]; export type DialogType = 'modal' | 'non-modal' | 'alert'; @@ -62,7 +60,7 @@ export type DialogProps = ComponentProps> & { * Callback fired when the component changes value from open state. * @default undefined */ - onOpenChange?: DialogOpenChangeEventListener; + onOpenChange?(...args: DialogOpenChangeArgs): 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. diff --git a/packages/react-components/react-dialog/src/components/Dialog/useDialog.test.ts b/packages/react-components/react-dialog/src/components/Dialog/useDialog.test.ts index 12be89f12c900..4c9cbe595c77d 100644 --- a/packages/react-components/react-dialog/src/components/Dialog/useDialog.test.ts +++ b/packages/react-components/react-dialog/src/components/Dialog/useDialog.test.ts @@ -1,18 +1,19 @@ import { act, renderHook } from '@testing-library/react-hooks'; import * as React from 'react'; -import { DialogOpenChangeEvent } from './Dialog.types'; +import type { DialogOpenChangeArgs } from './Dialog.types'; import { useDialog_unstable } from './useDialog'; describe('useAccordion_unstable', () => { it('handle open behavior', () => { + const ref = React.createRef(); const { result } = renderHook(() => - useDialog_unstable({ children: [React.createElement('div'), React.createElement('div')] }), + useDialog_unstable({ children: [React.createElement('div'), React.createElement('div')] }, ref), ); expect(result.current.open).toEqual(false); - const fakeEvent = { defaultPrevented: false } as DialogOpenChangeEvent; - act(() => result.current.requestOpenChange(fakeEvent, { open: true, type: 'triggerClick' })); + const fakeEvent = { defaultPrevented: false } as DialogOpenChangeArgs[0]; + act(() => result.current.requestOpenChange({ open: true, type: 'triggerClick', event: fakeEvent })); expect(result.current.open).toEqual(true); }); 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 bb4df368467a7..71205fb5243b3 100644 --- a/packages/react-components/react-dialog/src/components/Dialog/useDialog.ts +++ b/packages/react-components/react-dialog/src/components/Dialog/useDialog.ts @@ -5,8 +5,11 @@ import { useControllableState, useEventCallback, } from '@fluentui/react-utilities'; -import type { DialogOpenChangeEvent, DialogProps, DialogState } from './Dialog.types'; +import type { DialogOpenChangeArgs, DialogProps, DialogState, DialogType } from './Dialog.types'; import { DialogRequestOpenChangeData } from '../../contexts/dialogContext'; +import { Escape } from '@fluentui/keyboard-keys'; +import { useFocusFinders } from '@fluentui/react-tabster'; +import { useFluent_unstable } from '@fluentui/react-shared-contexts'; /** * Create the state required to render Dialog. @@ -17,21 +20,61 @@ import { DialogRequestOpenChangeData } from '../../contexts/dialogContext'; * @param props - props from this instance of Dialog */ export const useDialog_unstable = (props: DialogProps, ref: React.Ref): DialogState => { - const { children, open, defaultOpen, overlay, type = 'modal', onOpenChange, as } = props; + const { children, overlay, type = 'modal', onOpenChange, as = 'div' } = props; + const [trigger, content] = childrenToTriggerAndContent(children); - const [openValue, setOpenValue] = useControllableState({ - state: open, - defaultState: defaultOpen, + const [open, setOpen] = useControllableState({ + state: props.open, + defaultState: props.defaultOpen, initialState: false, }); + const rootShorthand = getNativeElementProps(as, { + ...props, + ref, + }); + + const overlayShorthand = resolveShorthand(overlay, { + required: type !== 'non-modal', + defaultProps: { + 'aria-hidden': 'true', + }, + }); + const requestOpenChange = useEventCallback((data: DialogRequestOpenChangeData) => { - const nextOpen = normalizeSetOpen(data.open)(openValue); - onOpenChange?.(data.event as DialogOpenChangeEvent, { open: nextOpen, type: data.type }); + const getNextOpen = normalizeSetStateAction(data.open); + const isDefaultPrevented = normalizeDefaultPrevented(data.event); + + onOpenChange?.( + ...([ + data.event, + { + type: data.type, + open: getNextOpen(open), + }, + ] as DialogOpenChangeArgs), + ); + // if user prevents default then do not change state value - if (!data.event.defaultPrevented) { - setOpenValue(nextOpen); + if (!isDefaultPrevented()) { + setOpen(getNextOpen); + } + }); + + const { contentRef, triggerRef } = useFocusFirstElement({ open, type, requestOpenChange }); + + const handleOverLayClick = useEventCallback((event: React.MouseEvent) => { + overlayShorthand?.onClick?.(event); + if (isOverlayClickDismiss(event, type)) { + requestOpenChange({ event, open: false, type: 'overlayClick' }); + } + }); + + const handleRootKeyDown = useEventCallback((event: React.KeyboardEvent) => { + rootShorthand.onKeyDown?.(event); + if (isEscapeKeyDismiss(event, type)) { + requestOpenChange({ event, open: false, type: 'escapeKeyDown' }); } }); @@ -40,18 +83,21 @@ export const useDialog_unstable = (props: DialogProps, ref: React.Ref) { +function normalizeSetStateAction(setOpen: React.SetStateAction): (prev: boolean) => boolean { return typeof setOpen === 'function' ? setOpen : () => setOpen; } + +/** + * normalizes defaultPrevented to work the same way between synthetic events and regular event + */ +function normalizeDefaultPrevented(event: React.SyntheticEvent | Event) { + if (event instanceof Event) { + return () => event.defaultPrevented; + } + return event.isDefaultPrevented; +} + +/** + * Checks if keydown event is a proper Escape key dismiss + */ +function isEscapeKeyDismiss(event: React.KeyboardEvent | KeyboardEvent, type: DialogType): boolean { + const isDefaultPrevented = normalizeDefaultPrevented(event); + return event.key === Escape && type !== 'alert' && !isDefaultPrevented(); +} + +/** + * Checks is click event is a proper Overlay click dismiss + */ +function isOverlayClickDismiss(event: React.MouseEvent, type: DialogType): boolean { + const isDefaultPrevented = normalizeDefaultPrevented(event); + return type === 'modal' && !isDefaultPrevented(); +} + +/** + * Focus first element on content when dialog is opened, + * in case there's no focusable element, then a eventlistener is added to document + * to ensure Escape keydown functionality + */ +function useFocusFirstElement({ + open, + requestOpenChange, + type, +}: Pick) { + const { findFirstFocusable } = useFocusFinders(); + const { targetDocument } = useFluent_unstable(); + const contentRef = React.useRef(null); + const triggerRef = React.useRef(null); + + React.useEffect(() => { + if (open) { + const element = contentRef.current && findFirstFocusable(contentRef.current); + if (element) { + element.focus(); + return; + } + // eslint-disable-next-line no-console + console.warn('A Dialog should have at least one focusable element inside DialogContent'); + + triggerRef.current?.blur(); + const listener = (event: KeyboardEvent) => { + if (isEscapeKeyDismiss(event, type)) { + requestOpenChange({ + event, + open: false, + type: 'documentEscapeKeyDown', + }); + triggerRef.current?.focus(); + } + }; + targetDocument?.addEventListener('keydown', listener); + return () => targetDocument?.removeEventListener('keydown', listener); + } + }, [findFirstFocusable, requestOpenChange, open, type, targetDocument]); + + return { contentRef, triggerRef }; +} 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 0a1983af57156..489ed4fb48375 100644 --- a/packages/react-components/react-dialog/src/components/Dialog/useDialogContextValues.ts +++ b/packages/react-components/react-dialog/src/components/Dialog/useDialogContextValues.ts @@ -2,13 +2,25 @@ import { DialogContextValue } from '../../contexts/dialogContext'; import type { DialogContextValues, DialogState } from './Dialog.types'; export function useDialogContextValues_unstable(state: DialogState): DialogContextValues { - const { type, open, requestOpenChange } = state; + const { + type, + open, + requestOpenChange, + triggerTabsterAttributes, + contentTabsterAttributes, + triggerRef, + contentRef, + } = state; // This context is created with "@fluentui/react-context-selector", these is no sense to memoize it const dialog: DialogContextValue = { type, open, requestOpenChange, + triggerTabsterAttributes, + contentTabsterAttributes, + triggerRef, + contentRef, }; return { dialog }; diff --git a/packages/react-components/react-dialog/src/components/Dialog/useDialogStyles.ts b/packages/react-components/react-dialog/src/components/Dialog/useDialogStyles.ts index f33e09fe26e25..d3a4f2bb5ef7f 100644 --- a/packages/react-components/react-dialog/src/components/Dialog/useDialogStyles.ts +++ b/packages/react-components/react-dialog/src/components/Dialog/useDialogStyles.ts @@ -10,8 +10,22 @@ export const dialogClassNames: SlotClassNames = { * Styles for the root slot */ const useStyles = makeStyles({ - root: {}, - overlay: {}, + root: { + position: 'fixed', + width: '100%', + height: '100%', + top: 0, + left: 0, + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + }, + overlay: { + position: 'absolute', + height: '100%', + width: '100%', + backgroundColor: 'rgba(0, 0, 0, 0.4)', + }, }); /** diff --git a/packages/react-components/react-dialog/src/components/DialogContent/DialogContent.tsx b/packages/react-components/react-dialog/src/components/DialogContent/DialogContent.tsx index 59eb12958e28d..474add2c8109a 100644 --- a/packages/react-components/react-dialog/src/components/DialogContent/DialogContent.tsx +++ b/packages/react-components/react-dialog/src/components/DialogContent/DialogContent.tsx @@ -6,7 +6,8 @@ import type { DialogContentProps } from './DialogContent.types'; import type { ForwardRefComponent } from '@fluentui/react-utilities'; /** - * DialogContent component - TODO: add more docs + * DialogContent component represents the visual part of a `Dialog` as a whole, + * it contains everything that should be visible. */ export const DialogContent: ForwardRefComponent = React.forwardRef((props, ref) => { const state = useDialogContent_unstable(props, ref); diff --git a/packages/react-components/react-dialog/src/components/DialogContent/DialogContent.types.ts b/packages/react-components/react-dialog/src/components/DialogContent/DialogContent.types.ts index d474b0cd1f9d1..5b0f1be9cda5d 100644 --- a/packages/react-components/react-dialog/src/components/DialogContent/DialogContent.types.ts +++ b/packages/react-components/react-dialog/src/components/DialogContent/DialogContent.types.ts @@ -7,11 +7,9 @@ export type DialogContentSlots = { /** * DialogContent Props */ -export type DialogContentProps = ComponentProps & {}; +export type DialogContentProps = ComponentProps; /** * State used in rendering DialogContent */ export type DialogContentState = ComponentState; -// TODO: Remove semicolon from previous line, uncomment next line, and provide union of props to pick from DialogContentProps. -// & Required> diff --git a/packages/react-components/react-dialog/src/components/DialogContent/renderDialogContent.tsx b/packages/react-components/react-dialog/src/components/DialogContent/renderDialogContent.tsx index 499afa482f8b7..56ef46582797d 100644 --- a/packages/react-components/react-dialog/src/components/DialogContent/renderDialogContent.tsx +++ b/packages/react-components/react-dialog/src/components/DialogContent/renderDialogContent.tsx @@ -8,6 +8,5 @@ import type { DialogContentState, DialogContentSlots } from './DialogContent.typ export const renderDialogContent_unstable = (state: DialogContentState) => { const { slots, slotProps } = getSlots(state); - // TODO Add additional slots in the appropriate place return ; }; diff --git a/packages/react-components/react-dialog/src/components/DialogContent/useDialogContent.ts b/packages/react-components/react-dialog/src/components/DialogContent/useDialogContent.ts index 0f9b311bb4fc3..981d107e7bc65 100644 --- a/packages/react-components/react-dialog/src/components/DialogContent/useDialogContent.ts +++ b/packages/react-components/react-dialog/src/components/DialogContent/useDialogContent.ts @@ -1,6 +1,8 @@ import * as React from 'react'; -import { getNativeElementProps } from '@fluentui/react-utilities'; +import { getNativeElementProps, useMergedRefs } from '@fluentui/react-utilities'; import type { DialogContentProps, DialogContentState } from './DialogContent.types'; +import { useDialogContext_unstable } from '../../contexts/dialogContext'; +import { useModalAttributes } from '@fluentui/react-tabster'; /** * Create the state required to render DialogContent. @@ -15,17 +17,23 @@ export const useDialogContent_unstable = ( props: DialogContentProps, ref: React.Ref, ): DialogContentState => { + const { as = 'div' } = props; + + const contentRef = useDialogContext_unstable(ctx => ctx.contentRef); + const type = useDialogContext_unstable(ctx => ctx.type); + + const { modalAttributes } = useModalAttributes({ + trapFocus: type !== 'non-modal', + }); + return { - // TODO add appropriate props/defaults components: { - // TODO add each slot's element type or component root: 'div', }, - // TODO add appropriate slots, for example: - // mySlot: resolveShorthand(props.mySlot), - root: getNativeElementProps('div', { - ref, + root: getNativeElementProps(as, { + ref: useMergedRefs(ref, contentRef), ...props, + ...modalAttributes, }), }; }; diff --git a/packages/react-components/react-dialog/src/components/DialogContent/useDialogContentStyles.ts b/packages/react-components/react-dialog/src/components/DialogContent/useDialogContentStyles.ts index cd4f6ba6f50a8..0089e5dc7f0ba 100644 --- a/packages/react-components/react-dialog/src/components/DialogContent/useDialogContentStyles.ts +++ b/packages/react-components/react-dialog/src/components/DialogContent/useDialogContentStyles.ts @@ -1,6 +1,7 @@ -import { makeStyles, mergeClasses } from '@griffel/react'; +import { makeStyles, mergeClasses, shorthands } from '@griffel/react'; import type { DialogContentSlots, DialogContentState } from './DialogContent.types'; import type { SlotClassNames } from '@fluentui/react-utilities'; +import { tokens } from '@fluentui/react-theme'; export const dialogContentClassNames: SlotClassNames = { root: 'fui-DialogContent', @@ -11,10 +12,18 @@ export const dialogContentClassNames: SlotClassNames = { */ const useStyles = makeStyles({ root: { - // TODO Add default styles for the root element + position: 'relative', + display: 'flex', + flexDirection: 'column', + width: 'fit-content', + height: 'fit-content', + maxWidth: '600px', + maxHeight: '100vh', + boxShadow: tokens.shadow64, + backgroundColor: tokens.colorNeutralBackground1, + ...shorthands.borderRadius('8px'), + ...shorthands.margin('auto'), }, - - // TODO add additional classes for different states and/or slots }); /** @@ -23,9 +32,5 @@ const useStyles = makeStyles({ export const useDialogContentStyles_unstable = (state: DialogContentState): DialogContentState => { const styles = useStyles(); state.root.className = mergeClasses(dialogContentClassNames.root, styles.root, state.root.className); - - // TODO Add class names to slots, for example: - // state.mySlot.className = mergeClasses(styles.mySlot, state.mySlot.className); - return state; }; diff --git a/packages/react-components/react-dialog/src/components/DialogTrigger/useDialogTrigger.ts b/packages/react-components/react-dialog/src/components/DialogTrigger/useDialogTrigger.ts index fbc71af42e73c..c1aec0389482a 100644 --- a/packages/react-components/react-dialog/src/components/DialogTrigger/useDialogTrigger.ts +++ b/packages/react-components/react-dialog/src/components/DialogTrigger/useDialogTrigger.ts @@ -1,4 +1,10 @@ -import { applyTriggerPropsToChildren, getTriggerChild, useEventCallback } from '@fluentui/react-utilities'; +import { useModalAttributes } from '@fluentui/react-tabster'; +import { + applyTriggerPropsToChildren, + getTriggerChild, + useEventCallback, + useMergedRefs, +} from '@fluentui/react-utilities'; import * as React from 'react'; import { useDialogContext_unstable } from '../../contexts/dialogContext'; import { @@ -16,19 +22,24 @@ import { */ export const useDialogTrigger_unstable = (props: DialogTriggerProps): DialogTriggerState => { const { children, type = 'toggle' } = props; + const child = React.isValidElement(children) ? getTriggerChild(children) : undefined; const requestOpenChange = useDialogContext_unstable(ctx => ctx.requestOpenChange); + const triggerRef = useDialogContext_unstable(ctx => ctx.triggerRef); + const { triggerAttributes } = useModalAttributes(); - const handleClick = useEventCallback((ev: React.MouseEvent) => { - child?.props.onClick?.(ev); - requestOpenChange({ event: ev, type: 'triggerClick', open: updateOpen(type) }); + const handleClick = useEventCallback((event: React.MouseEvent) => { + child?.props.onClick?.(event); + requestOpenChange({ event, type: 'triggerClick', open: updateOpen(type) }); }); return { children: applyTriggerPropsToChildren(children, { 'aria-haspopup': 'dialog', + ref: useMergedRefs(child?.ref, triggerRef), onClick: handleClick, + ...triggerAttributes, }), }; }; diff --git a/packages/react-components/react-dialog/src/contexts/dialogContext.ts b/packages/react-components/react-dialog/src/contexts/dialogContext.ts index f1693042dad63..4a7b68db7f950 100644 --- a/packages/react-components/react-dialog/src/contexts/dialogContext.ts +++ b/packages/react-components/react-dialog/src/contexts/dialogContext.ts @@ -1,37 +1,29 @@ import { createContext, ContextSelector, useContextSelector } from '@fluentui/react-context-selector'; import type { Context } from '@fluentui/react-context-selector'; -import type { DialogType } from '../Dialog'; +import type { DialogOpenChangeArgs, DialogType } from '../Dialog'; import * as React from 'react'; -export type DialogRequestOpenChangeSourceType = 'escapeKeyDown' | 'overlayClick' | 'triggerClick'; - export type DialogRequestOpenChangeData = { - /** - * The event that originated the change - */ - event: React.SyntheticEvent; - /** - * The source type of the callback invocation - */ - type: DialogRequestOpenChangeSourceType; - /** - * The next value for the internal state of the dialog or a function to update it - */ + event: DialogOpenChangeArgs[0]; open: React.SetStateAction; -}; +} & Pick; export type DialogContextValue = { + triggerRef: React.Ref; + contentRef: React.Ref; type: DialogType; open: boolean; /** * Requests dialog main component to update it's internal open state */ - requestOpenChange(data: DialogRequestOpenChangeData): void; + requestOpenChange: (data: DialogRequestOpenChangeData) => void; }; export const DialogContext: Context = createContext({ open: false, type: 'modal', + triggerRef: { current: null }, + contentRef: { current: null }, requestOpenChange() { /* noop */ }, diff --git a/packages/react-components/react-dialog/src/index.ts b/packages/react-components/react-dialog/src/index.ts index 1ae5803440ed0..6652623cdaca4 100644 --- a/packages/react-components/react-dialog/src/index.ts +++ b/packages/react-components/react-dialog/src/index.ts @@ -24,3 +24,12 @@ export { } from './DialogContent'; export type { DialogContentProps, DialogContentSlots, DialogContentState } from './DialogContent'; + +export { + DialogTitle, + dialogTitleClassNames, + useDialogTitleStyles_unstable, + useDialogTitle_unstable, + renderDialogTitle_unstable, +} from './DialogTitle'; +export type { DialogTitleProps, DialogTitleSlots, DialogTitleState } from './DialogTitle'; diff --git a/packages/react-components/react-dialog/src/stories/Dialog/DialogDefault.stories.tsx b/packages/react-components/react-dialog/src/stories/Dialog/DialogDefault.stories.tsx index 7bb120a4581ab..081fb956b197a 100644 --- a/packages/react-components/react-dialog/src/stories/Dialog/DialogDefault.stories.tsx +++ b/packages/react-components/react-dialog/src/stories/Dialog/DialogDefault.stories.tsx @@ -1,12 +1,24 @@ import * as React from 'react'; -import { Dialog, DialogTrigger, DialogContent } from '@fluentui/react-dialog'; +import { Dialog, DialogTrigger, DialogContent, DialogTitle } from '@fluentui/react-dialog'; import type { DialogProps } from '@fluentui/react-dialog'; +import { Button } from "@fluentui/react-components"; -export const Default = (props: Partial) => ( - - - - - Dialog Content - -); +export const Default = (props: Partial) => { + const dialogRef = React.useRef(null); + return ( + <> + + + + + + + {/* */} + Dialog Content + + + + oi + + ); +}; From e2a8d54036b7d10881e84f9f40aff5d93e69c605 Mon Sep 17 00:00:00 2001 From: Bernardo Sunderhus Date: Thu, 30 Jun 2022 07:58:58 +0000 Subject: [PATCH 06/17] changes types props to more descriptive names --- .../react-components/react-dialog/Spec.md | 7 ++- .../react-dialog/etc/react-dialog.api.md | 11 ++-- .../src/components/Dialog/Dialog.types.ts | 6 ++- .../src/components/Dialog/useDialog.ts | 53 ++++++++++--------- .../Dialog/useDialogContextValues.ts | 19 +++---- .../DialogContent/DialogContent.types.ts | 2 +- .../DialogContent/useDialogContent.ts | 2 +- .../DialogTrigger/DialogTrigger.types.ts | 4 +- .../DialogTrigger/useDialogTrigger.ts | 8 +-- .../src/contexts/dialogContext.ts | 6 +-- .../react-dialog/src/index.ts | 2 +- 11 files changed, 60 insertions(+), 60 deletions(-) diff --git a/packages/react-components/react-dialog/Spec.md b/packages/react-components/react-dialog/Spec.md index fe1a12c630ca5..411bb04b94ab9 100644 --- a/packages/react-components/react-dialog/Spec.md +++ b/packages/react-components/react-dialog/Spec.md @@ -106,8 +106,10 @@ type DialogProps = ComponentProps & { * `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 @@ -148,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 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 e19683c6dc47a..399c2c445a611 100644 --- a/packages/react-components/react-dialog/etc/react-dialog.api.md +++ b/packages/react-components/react-dialog/etc/react-dialog.api.md @@ -13,7 +13,6 @@ import * as React_2 from 'react'; import { ReactElement } from 'react'; import type { Slot } from '@fluentui/react-utilities'; import type { SlotClassNames } from '@fluentui/react-utilities'; -import { Types } from '@fluentui/react-tabster'; // @public (undocumented) export const Dialog: ForwardRefComponent; @@ -40,7 +39,7 @@ export type DialogContentState = ComponentState; // @public (undocumented) export type DialogProps = ComponentProps> & { - type?: DialogType; + modalType?: DialogModalType; open?: boolean; defaultOpen?: boolean; onOpenChange?(...args: DialogOpenChangeArgs): void; @@ -79,6 +78,9 @@ export type DialogTitleState = ComponentState; // @public export const DialogTrigger: React_2.FC & FluentTriggerComponent; +// @public (undocumented) +export type DialogTriggerAction = 'open' | 'close' | 'toggle'; + // @public export type DialogTriggerChildProps = Required, 'onClick' | 'aria-haspopup'>> & { ref?: React_2.Ref; @@ -86,7 +88,7 @@ export type DialogTriggerChildProps = Required; }) | ((props: DialogTriggerChildProps) => React_2.ReactElement | null); @@ -97,9 +99,6 @@ export type DialogTriggerState = { children: React_2.ReactElement | null; }; -// @public (undocumented) -export type DialogTriggerType = 'open' | 'close' | 'toggle'; - // @public export const renderDialog_unstable: (state: DialogState, contextValues: DialogContextValues) => JSX.Element; 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 8c7055116591f..6c0744253caeb 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 @@ -22,7 +22,7 @@ export type DialogOpenChangeArgs = | [event: React.MouseEvent, data: { type: 'overlayClick'; open: boolean }] | [event: React.MouseEvent, data: { type: 'triggerClick'; open: boolean }]; -export type DialogType = 'modal' | 'non-modal' | 'alert'; +export type DialogModalType = 'modal' | 'non-modal' | 'alert'; export type DialogContextValues = { dialog: DialogContextValue; @@ -44,8 +44,10 @@ export type DialogProps = ComponentProps> & { * 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?: DialogType; + modalType?: DialogModalType; /** * Controls the open state of the dialog * @default undefined 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 71205fb5243b3..3e37d3def419a 100644 --- a/packages/react-components/react-dialog/src/components/Dialog/useDialog.ts +++ b/packages/react-components/react-dialog/src/components/Dialog/useDialog.ts @@ -5,7 +5,7 @@ import { useControllableState, useEventCallback, } from '@fluentui/react-utilities'; -import type { DialogOpenChangeArgs, DialogProps, DialogState, DialogType } from './Dialog.types'; +import type { DialogOpenChangeArgs, DialogProps, DialogState, DialogModalType } from './Dialog.types'; import { DialogRequestOpenChangeData } from '../../contexts/dialogContext'; import { Escape } from '@fluentui/keyboard-keys'; import { useFocusFinders } from '@fluentui/react-tabster'; @@ -20,7 +20,7 @@ import { useFluent_unstable } from '@fluentui/react-shared-contexts'; * @param props - props from this instance of Dialog */ export const useDialog_unstable = (props: DialogProps, ref: React.Ref): DialogState => { - const { children, overlay, type = 'modal', onOpenChange, as = 'div' } = props; + const { children, overlay, modalType = 'modal', onOpenChange, as = 'div' } = props; const [trigger, content] = childrenToTriggerAndContent(children); @@ -36,7 +36,7 @@ export const useDialog_unstable = (props: DialogProps, ref: React.Ref) => { overlayShorthand?.onClick?.(event); - if (isOverlayClickDismiss(event, type)) { + if (isOverlayClickDismiss(event, modalType)) { requestOpenChange({ event, open: false, type: 'overlayClick' }); } }); const handleRootKeyDown = useEventCallback((event: React.KeyboardEvent) => { rootShorthand.onKeyDown?.(event); - if (isEscapeKeyDismiss(event, type)) { + if (isEscapeKeyDismiss(event, modalType)) { requestOpenChange({ event, open: false, type: 'escapeKeyDown' }); } }); @@ -92,7 +92,7 @@ export const useDialog_unstable = (props: DialogProps, ref: React.Ref) { + modalType, +}: Pick) { const { findFirstFocusable } = useFocusFinders(); const { targetDocument } = useFluent_unstable(); const contentRef = React.useRef(null); @@ -188,21 +188,24 @@ function useFocusFirstElement({ // eslint-disable-next-line no-console console.warn('A Dialog should have at least one focusable element inside DialogContent'); - triggerRef.current?.blur(); - const listener = (event: KeyboardEvent) => { - if (isEscapeKeyDismiss(event, type)) { - requestOpenChange({ - event, - open: false, - type: 'documentEscapeKeyDown', - }); - triggerRef.current?.focus(); - } - }; - targetDocument?.addEventListener('keydown', listener); - return () => targetDocument?.removeEventListener('keydown', listener); + if (triggerRef.current && targetDocument) { + const trigger = triggerRef.current; + trigger.blur(); + const listener = (event: KeyboardEvent) => { + if (isEscapeKeyDismiss(event, modalType)) { + requestOpenChange({ + event, + open: false, + type: 'documentEscapeKeyDown', + }); + trigger.focus(); + } + }; + targetDocument.addEventListener('keydown', listener); + return () => targetDocument.removeEventListener('keydown', listener); + } } - }, [findFirstFocusable, requestOpenChange, open, type, targetDocument]); + }, [findFirstFocusable, requestOpenChange, open, modalType, targetDocument]); return { contentRef, triggerRef }; } 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 489ed4fb48375..f5742e1b62153 100644 --- a/packages/react-components/react-dialog/src/components/Dialog/useDialogContextValues.ts +++ b/packages/react-components/react-dialog/src/components/Dialog/useDialogContextValues.ts @@ -2,23 +2,16 @@ import { DialogContextValue } from '../../contexts/dialogContext'; import type { DialogContextValues, DialogState } from './Dialog.types'; export function useDialogContextValues_unstable(state: DialogState): DialogContextValues { - const { - type, - open, - requestOpenChange, - triggerTabsterAttributes, - contentTabsterAttributes, - triggerRef, - contentRef, - } = state; + const { modalType, open, requestOpenChange, triggerRef, contentRef } = state; - // This context is created with "@fluentui/react-context-selector", these is no sense to memoize it + /** + * This context is created with "@fluentui/react-context-selector", + * there is no sense to memoize it + */ const dialog: DialogContextValue = { - type, + modalType, open, requestOpenChange, - triggerTabsterAttributes, - contentTabsterAttributes, triggerRef, contentRef, }; diff --git a/packages/react-components/react-dialog/src/components/DialogContent/DialogContent.types.ts b/packages/react-components/react-dialog/src/components/DialogContent/DialogContent.types.ts index 5b0f1be9cda5d..3142ada984df3 100644 --- a/packages/react-components/react-dialog/src/components/DialogContent/DialogContent.types.ts +++ b/packages/react-components/react-dialog/src/components/DialogContent/DialogContent.types.ts @@ -1,7 +1,7 @@ import type { ComponentProps, ComponentState, Slot } from '@fluentui/react-utilities'; export type DialogContentSlots = { - root: Slot<'div'>; + root: Slot<'div', 'main'>; }; /** diff --git a/packages/react-components/react-dialog/src/components/DialogContent/useDialogContent.ts b/packages/react-components/react-dialog/src/components/DialogContent/useDialogContent.ts index 981d107e7bc65..138abbc6943a1 100644 --- a/packages/react-components/react-dialog/src/components/DialogContent/useDialogContent.ts +++ b/packages/react-components/react-dialog/src/components/DialogContent/useDialogContent.ts @@ -20,7 +20,7 @@ export const useDialogContent_unstable = ( const { as = 'div' } = props; const contentRef = useDialogContext_unstable(ctx => ctx.contentRef); - const type = useDialogContext_unstable(ctx => ctx.type); + const type = useDialogContext_unstable(ctx => ctx.modalType); const { modalAttributes } = useModalAttributes({ trapFocus: type !== 'non-modal', diff --git a/packages/react-components/react-dialog/src/components/DialogTrigger/DialogTrigger.types.ts b/packages/react-components/react-dialog/src/components/DialogTrigger/DialogTrigger.types.ts index 942bc9d634e31..4d5bccb07bb6d 100644 --- a/packages/react-components/react-dialog/src/components/DialogTrigger/DialogTrigger.types.ts +++ b/packages/react-components/react-dialog/src/components/DialogTrigger/DialogTrigger.types.ts @@ -1,6 +1,6 @@ import * as React from 'react'; -export type DialogTriggerType = 'open' | 'close' | 'toggle'; +export type DialogTriggerAction = 'open' | 'close' | 'toggle'; export type DialogTriggerProps = { /** @@ -8,7 +8,7 @@ export type DialogTriggerProps = { * closing or toggling a Dialog visibility state. * @default 'toggle' */ - type?: DialogTriggerType; + action?: DialogTriggerAction; /** * Explicitly require single child or render function * to inject properties diff --git a/packages/react-components/react-dialog/src/components/DialogTrigger/useDialogTrigger.ts b/packages/react-components/react-dialog/src/components/DialogTrigger/useDialogTrigger.ts index c1aec0389482a..18c931bd34271 100644 --- a/packages/react-components/react-dialog/src/components/DialogTrigger/useDialogTrigger.ts +++ b/packages/react-components/react-dialog/src/components/DialogTrigger/useDialogTrigger.ts @@ -11,7 +11,7 @@ import { DialogTriggerChildProps, DialogTriggerProps, DialogTriggerState, - DialogTriggerType, + DialogTriggerAction, } from './DialogTrigger.types'; /** @@ -21,7 +21,7 @@ import { * @param props - props from this instance of DialogTrigger */ export const useDialogTrigger_unstable = (props: DialogTriggerProps): DialogTriggerState => { - const { children, type = 'toggle' } = props; + const { children, action = 'toggle' } = props; const child = React.isValidElement(children) ? getTriggerChild(children) : undefined; @@ -31,7 +31,7 @@ export const useDialogTrigger_unstable = (props: DialogTriggerProps): DialogTrig const handleClick = useEventCallback((event: React.MouseEvent) => { child?.props.onClick?.(event); - requestOpenChange({ event, type: 'triggerClick', open: updateOpen(type) }); + requestOpenChange({ event, type: 'triggerClick', open: updateOpen(action) }); }); return { @@ -44,7 +44,7 @@ export const useDialogTrigger_unstable = (props: DialogTriggerProps): DialogTrig }; }; -function updateOpen(type: DialogTriggerType): React.SetStateAction { +function updateOpen(type: DialogTriggerAction): React.SetStateAction { switch (type) { case 'close': return false; diff --git a/packages/react-components/react-dialog/src/contexts/dialogContext.ts b/packages/react-components/react-dialog/src/contexts/dialogContext.ts index 4a7b68db7f950..5a8ea61d00add 100644 --- a/packages/react-components/react-dialog/src/contexts/dialogContext.ts +++ b/packages/react-components/react-dialog/src/contexts/dialogContext.ts @@ -1,6 +1,6 @@ import { createContext, ContextSelector, useContextSelector } from '@fluentui/react-context-selector'; import type { Context } from '@fluentui/react-context-selector'; -import type { DialogOpenChangeArgs, DialogType } from '../Dialog'; +import type { DialogOpenChangeArgs, DialogModalType } from '../Dialog'; import * as React from 'react'; export type DialogRequestOpenChangeData = { @@ -11,7 +11,7 @@ export type DialogRequestOpenChangeData = { export type DialogContextValue = { triggerRef: React.Ref; contentRef: React.Ref; - type: DialogType; + modalType: DialogModalType; open: boolean; /** * Requests dialog main component to update it's internal open state @@ -21,7 +21,7 @@ export type DialogContextValue = { export const DialogContext: Context = createContext({ open: false, - type: 'modal', + modalType: 'modal', triggerRef: { current: null }, contentRef: { current: null }, requestOpenChange() { diff --git a/packages/react-components/react-dialog/src/index.ts b/packages/react-components/react-dialog/src/index.ts index 6652623cdaca4..2ad28657ba097 100644 --- a/packages/react-components/react-dialog/src/index.ts +++ b/packages/react-components/react-dialog/src/index.ts @@ -12,7 +12,7 @@ export type { DialogTriggerProps, DialogTriggerChildProps, DialogTriggerState, - DialogTriggerType, + DialogTriggerAction, } from './DialogTrigger'; export { From 8e0ce64074330f823d5b8b3da8896106d9eacf5c Mon Sep 17 00:00:00 2001 From: Bernardo Sunderhus Date: Thu, 7 Jul 2022 13:34:36 +0000 Subject: [PATCH 07/17] Updates stories --- packages/react-components/react-dialog/etc/react-dialog.api.md | 2 +- .../react-dialog/src/stories/Dialog/DialogDefault.stories.tsx | 2 +- .../react-dialog/src/stories/Dialog/DialogDescription.md | 1 + .../react-dialog/src/stories/DialogDescription.md | 1 - 4 files changed, 3 insertions(+), 3 deletions(-) delete mode 100644 packages/react-components/react-dialog/src/stories/DialogDescription.md 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 399c2c445a611..6a3ee045f41a9 100644 --- a/packages/react-components/react-dialog/etc/react-dialog.api.md +++ b/packages/react-components/react-dialog/etc/react-dialog.api.md @@ -31,7 +31,7 @@ export type DialogContentProps = ComponentProps; // @public (undocumented) export type DialogContentSlots = { - root: Slot<'div'>; + root: Slot<'div', 'main'>; }; // @public diff --git a/packages/react-components/react-dialog/src/stories/Dialog/DialogDefault.stories.tsx b/packages/react-components/react-dialog/src/stories/Dialog/DialogDefault.stories.tsx index 081fb956b197a..4604230f32e92 100644 --- a/packages/react-components/react-dialog/src/stories/Dialog/DialogDefault.stories.tsx +++ b/packages/react-components/react-dialog/src/stories/Dialog/DialogDefault.stories.tsx @@ -1,7 +1,7 @@ import * as React from 'react'; import { Dialog, DialogTrigger, DialogContent, DialogTitle } from '@fluentui/react-dialog'; import type { DialogProps } from '@fluentui/react-dialog'; -import { Button } from "@fluentui/react-components"; +import { Button } from '@fluentui/react-components'; export const Default = (props: Partial) => { const dialogRef = React.useRef(null); diff --git a/packages/react-components/react-dialog/src/stories/Dialog/DialogDescription.md b/packages/react-components/react-dialog/src/stories/Dialog/DialogDescription.md index e69de29bb2d1d..041ae3715e4f9 100644 --- a/packages/react-components/react-dialog/src/stories/Dialog/DialogDescription.md +++ b/packages/react-components/react-dialog/src/stories/Dialog/DialogDescription.md @@ -0,0 +1 @@ +`Dialog` is a window overlaid on either the primary window or another dialog window. Windows under a modal dialog are inert. That is, users cannot interact with content outside an active dialog window. Inert content outside an active dialog is typically visually obscured or dimmed so it is difficult to discern, and in some implementations, attempts to interact with the inert content cause the dialog to close. diff --git a/packages/react-components/react-dialog/src/stories/DialogDescription.md b/packages/react-components/react-dialog/src/stories/DialogDescription.md deleted file mode 100644 index 041ae3715e4f9..0000000000000 --- a/packages/react-components/react-dialog/src/stories/DialogDescription.md +++ /dev/null @@ -1 +0,0 @@ -`Dialog` is a window overlaid on either the primary window or another dialog window. Windows under a modal dialog are inert. That is, users cannot interact with content outside an active dialog window. Inert content outside an active dialog is typically visually obscured or dimmed so it is difficult to discern, and in some implementations, attempts to interact with the inert content cause the dialog to close. From 4e18beb05f1066f322a06c12f75217174dcfb8c2 Mon Sep 17 00:00:00 2001 From: Bernardo Sunderhus Date: Mon, 11 Jul 2022 11:54:48 +0000 Subject: [PATCH 08/17] Adds DialogTitle styles --- .../react-dialog/etc/react-dialog.api.md | 6 ++- .../react-dialog/package.json | 16 ++++--- .../src/components/Dialog/Dialog.types.ts | 4 +- .../src/components/Dialog/renderDialog.tsx | 2 +- .../src/components/Dialog/useDialog.ts | 43 ++++++++++--------- .../DialogTitle/DialogTitle.types.ts | 9 ++-- .../DialogTitle/renderDialogTitle.tsx | 13 +++++- .../{useDialogTitle.ts => useDialogTitle.tsx} | 15 ++++++- .../DialogTitle/useDialogTitleStyles.ts | 28 +++++++++++- .../DialogTrigger/useDialogTrigger.ts | 8 +++- .../stories/Dialog/DialogDefault.stories.tsx | 9 ++-- 11 files changed, 108 insertions(+), 45 deletions(-) rename packages/react-components/react-dialog/src/components/DialogTitle/{useDialogTitle.ts => useDialogTitle.tsx} (56%) 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 6a3ee045f41a9..cbcf89a6bc462 100644 --- a/packages/react-components/react-dialog/etc/react-dialog.api.md +++ b/packages/react-components/react-dialog/etc/react-dialog.api.md @@ -4,6 +4,9 @@ ```ts +/// + +import { ARIAButtonSlotProps } from '@fluentui/react-aria'; import type { ComponentProps } from '@fluentui/react-utilities'; import type { ComponentState } from '@fluentui/react-utilities'; import type { FluentTriggerComponent } from '@fluentui/react-utilities'; @@ -69,7 +72,8 @@ export type DialogTitleProps = ComponentProps & {}; // @public (undocumented) export type DialogTitleSlots = { - root: Slot<'div'>; + root: Slot<'div', 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6'>; + closeButton?: Slot; }; // @public diff --git a/packages/react-components/react-dialog/package.json b/packages/react-components/react-dialog/package.json index 5998f520108c9..cf4551ac566cb 100644 --- a/packages/react-components/react-dialog/package.json +++ b/packages/react-components/react-dialog/package.json @@ -33,13 +33,15 @@ }, "dependencies": { "@griffel/react": "^1.2.0", - "@fluentui/keyboard-keys": "9.0.0-rc.6", - "@fluentui/react-context-selector": "9.0.0-rc.10", - "@fluentui/react-shared-contexts": "9.0.0-rc.10", - "@fluentui/react-tabster": "9.0.0-rc.13", - "@fluentui/react-theme": "9.0.0-rc.9", - "@fluentui/react-portal": "9.0.0-rc.13", - "@fluentui/react-utilities": "9.0.0-rc.10", + "@fluentui/keyboard-keys": "9.0.0", + "@fluentui/react-context-selector": "9.0.0", + "@fluentui/react-shared-contexts": "9.0.0", + "@fluentui/react-aria": "9.0.0", + "@fluentui/react-icons": "^2.0.175", + "@fluentui/react-tabster": "9.0.1", + "@fluentui/react-theme": "9.0.0", + "@fluentui/react-portal": "9.0.1", + "@fluentui/react-utilities": "9.0.0", "tslib": "^2.1.0" }, "peerDependencies": { 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 6c0744253caeb..dbe5334cf4928 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 @@ -9,8 +9,8 @@ export type DialogSlots = { * This slot expects a `
` element which will replace the default overlay. * The overlay should have `aria-hidden="true"`. */ - overlay: Slot<'div'>; - root: Slot<'div'>; + overlay?: Slot<'div'>; + root: NonNullable>; }; export type DialogOpenChangeArgs = diff --git a/packages/react-components/react-dialog/src/components/Dialog/renderDialog.tsx b/packages/react-components/react-dialog/src/components/Dialog/renderDialog.tsx index 22ef19422e25f..e4f6736f42314 100644 --- a/packages/react-components/react-dialog/src/components/Dialog/renderDialog.tsx +++ b/packages/react-components/react-dialog/src/components/Dialog/renderDialog.tsx @@ -17,7 +17,7 @@ export const renderDialog_unstable = (state: DialogState, contextValues: DialogC {open && ( - + {slots.overlay && } {content} 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 3e37d3def419a..07fbe4a773171 100644 --- a/packages/react-components/react-dialog/src/components/Dialog/useDialog.ts +++ b/packages/react-components/react-dialog/src/components/Dialog/useDialog.ts @@ -183,27 +183,30 @@ function useFocusFirstElement({ const element = contentRef.current && findFirstFocusable(contentRef.current); if (element) { element.focus(); - return; - } - // eslint-disable-next-line no-console - console.warn('A Dialog should have at least one focusable element inside DialogContent'); - - if (triggerRef.current && targetDocument) { - const trigger = triggerRef.current; - trigger.blur(); - const listener = (event: KeyboardEvent) => { - if (isEscapeKeyDismiss(event, modalType)) { - requestOpenChange({ - event, - open: false, - type: 'documentEscapeKeyDown', - }); - trigger.focus(); - } - }; - targetDocument.addEventListener('keydown', listener); - return () => targetDocument.removeEventListener('keydown', listener); + if (modalType !== 'non-modal') { + return; + } + } else { + // eslint-disable-next-line no-console + console.warn('A Dialog should have at least one focusable element inside DialogContent'); } + + // if (triggerRef.current && targetDocument) { + // const trigger = triggerRef.current; + // trigger.blur(); + // const listener = (event: KeyboardEvent) => { + // if (isEscapeKeyDismiss(event, modalType)) { + // requestOpenChange({ + // event, + // open: false, + // type: 'documentEscapeKeyDown', + // }); + // trigger.focus(); + // } + // }; + // targetDocument.addEventListener('keydown', listener); + // return () => targetDocument.removeEventListener('keydown', listener); + // } } }, [findFirstFocusable, requestOpenChange, open, modalType, targetDocument]); diff --git a/packages/react-components/react-dialog/src/components/DialogTitle/DialogTitle.types.ts b/packages/react-components/react-dialog/src/components/DialogTitle/DialogTitle.types.ts index 1c2e9f6b164cd..115cee6d28c5d 100644 --- a/packages/react-components/react-dialog/src/components/DialogTitle/DialogTitle.types.ts +++ b/packages/react-components/react-dialog/src/components/DialogTitle/DialogTitle.types.ts @@ -1,7 +1,12 @@ +import { ARIAButtonSlotProps } from '@fluentui/react-aria'; import type { ComponentProps, ComponentState, Slot } from '@fluentui/react-utilities'; export type DialogTitleSlots = { - root: Slot<'div'>; + /** + * By default this is a div, but can be a heading. + */ + root: Slot<'div', 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6'>; + closeButton?: Slot; }; /** @@ -13,5 +18,3 @@ export type DialogTitleProps = ComponentProps & {}; * State used in rendering DialogTitle */ export type DialogTitleState = ComponentState; -// TODO: Remove semicolon from previous line, uncomment next line, and provide union of props to pick from DialogTitleProps. -// & Required> diff --git a/packages/react-components/react-dialog/src/components/DialogTitle/renderDialogTitle.tsx b/packages/react-components/react-dialog/src/components/DialogTitle/renderDialogTitle.tsx index 2fd3a1d2a95d3..d0ba716a79655 100644 --- a/packages/react-components/react-dialog/src/components/DialogTitle/renderDialogTitle.tsx +++ b/packages/react-components/react-dialog/src/components/DialogTitle/renderDialogTitle.tsx @@ -1,6 +1,7 @@ import * as React from 'react'; import { getSlots } from '@fluentui/react-utilities'; import type { DialogTitleState, DialogTitleSlots } from './DialogTitle.types'; +import { DialogTrigger } from '../DialogTrigger'; /** * Render the final JSX of DialogTitle @@ -8,6 +9,14 @@ import type { DialogTitleState, DialogTitleSlots } from './DialogTitle.types'; export const renderDialogTitle_unstable = (state: DialogTitleState) => { const { slots, slotProps } = getSlots(state); - // TODO Add additional slots in the appropriate place - return ; + return ( + + {slotProps.root.children} + {slots.closeButton && ( + + + + )} + + ); }; diff --git a/packages/react-components/react-dialog/src/components/DialogTitle/useDialogTitle.ts b/packages/react-components/react-dialog/src/components/DialogTitle/useDialogTitle.tsx similarity index 56% rename from packages/react-components/react-dialog/src/components/DialogTitle/useDialogTitle.ts rename to packages/react-components/react-dialog/src/components/DialogTitle/useDialogTitle.tsx index 7120b2106dfba..df38196d7922b 100644 --- a/packages/react-components/react-dialog/src/components/DialogTitle/useDialogTitle.ts +++ b/packages/react-components/react-dialog/src/components/DialogTitle/useDialogTitle.tsx @@ -1,6 +1,9 @@ import * as React from 'react'; import { getNativeElementProps } from '@fluentui/react-utilities'; import type { DialogTitleProps, DialogTitleState } from './DialogTitle.types'; +import { useARIAButton } from '@fluentui/react-aria'; +import { useDialogContext_unstable } from '../../contexts/dialogContext'; +import { Dismiss24Regular } from '@fluentui/react-icons'; /** * Create the state required to render DialogTitle. @@ -12,14 +15,24 @@ import type { DialogTitleProps, DialogTitleState } from './DialogTitle.types'; * @param ref - reference to root HTMLElement of DialogTitle */ export const useDialogTitle_unstable = (props: DialogTitleProps, ref: React.Ref): DialogTitleState => { - const { as = 'div' } = props; + const { as = 'div', closeButton } = props; + const modalType = useDialogContext_unstable(ctx => ctx.modalType); + return { components: { root: 'div', + closeButton: 'button', }, root: getNativeElementProps(as, { ref, ...props, }), + closeButton: useARIAButton(closeButton, { + required: modalType === 'non-modal', + defaultProps: { + type: 'button', // This is added because the default for type is 'submit' + children: , + }, + }), }; }; diff --git a/packages/react-components/react-dialog/src/components/DialogTitle/useDialogTitleStyles.ts b/packages/react-components/react-dialog/src/components/DialogTitle/useDialogTitleStyles.ts index 33540c9ed52d3..826e0ca32ee14 100644 --- a/packages/react-components/react-dialog/src/components/DialogTitle/useDialogTitleStyles.ts +++ b/packages/react-components/react-dialog/src/components/DialogTitle/useDialogTitleStyles.ts @@ -1,9 +1,11 @@ -import { makeStyles, mergeClasses } from '@griffel/react'; +import { makeStyles, mergeClasses, shorthands } from '@griffel/react'; import type { DialogTitleSlots, DialogTitleState } from './DialogTitle.types'; import type { SlotClassNames } from '@fluentui/react-utilities'; +import { createFocusOutlineStyle } from '@fluentui/react-tabster'; export const dialogTitleClassNames: SlotClassNames = { root: 'fui-DialogTitle', + closeButton: 'fui-DialogTitle__closeButton', }; /** @@ -11,6 +13,21 @@ export const dialogTitleClassNames: SlotClassNames = { */ const useStyles = makeStyles({ root: {}, + closeButton: {}, + focusIndicator: createFocusOutlineStyle(), + // TODO: this should be extracted to another package + resetButton: { + // boxSizing: 'content-box', + // backgroundColor: 'inherit', + // color: 'inherit', + // fontFamily: 'inherit', + // fontSize: 'inherit', + // lineHeight: 'normal', + // ...shorthands.overflow('visible'), + // ...shorthands.padding(0), + // WebkitAppearance: 'button', + // textAlign: 'unset', + }, }); /** @@ -19,5 +36,14 @@ const useStyles = makeStyles({ export const useDialogTitleStyles_unstable = (state: DialogTitleState): DialogTitleState => { const styles = useStyles(); state.root.className = mergeClasses(dialogTitleClassNames.root, styles.root, state.root.className); + if (state.closeButton) { + state.closeButton.className = mergeClasses( + dialogTitleClassNames.closeButton, + styles.closeButton, + styles.resetButton, + styles.focusIndicator, + state.closeButton.className, + ); + } return state; }; diff --git a/packages/react-components/react-dialog/src/components/DialogTrigger/useDialogTrigger.ts b/packages/react-components/react-dialog/src/components/DialogTrigger/useDialogTrigger.ts index 18c931bd34271..3be190daa4f3c 100644 --- a/packages/react-components/react-dialog/src/components/DialogTrigger/useDialogTrigger.ts +++ b/packages/react-components/react-dialog/src/components/DialogTrigger/useDialogTrigger.ts @@ -31,7 +31,13 @@ export const useDialogTrigger_unstable = (props: DialogTriggerProps): DialogTrig const handleClick = useEventCallback((event: React.MouseEvent) => { child?.props.onClick?.(event); - requestOpenChange({ event, type: 'triggerClick', open: updateOpen(action) }); + if (!event.isDefaultPrevented()) { + requestOpenChange({ + event, + type: 'triggerClick', + open: updateOpen(action), + }); + } }); return { diff --git a/packages/react-components/react-dialog/src/stories/Dialog/DialogDefault.stories.tsx b/packages/react-components/react-dialog/src/stories/Dialog/DialogDefault.stories.tsx index 4604230f32e92..97a948c59a32b 100644 --- a/packages/react-components/react-dialog/src/stories/Dialog/DialogDefault.stories.tsx +++ b/packages/react-components/react-dialog/src/stories/Dialog/DialogDefault.stories.tsx @@ -4,21 +4,18 @@ import type { DialogProps } from '@fluentui/react-dialog'; import { Button } from '@fluentui/react-components'; export const Default = (props: Partial) => { - const dialogRef = React.useRef(null); return ( <> - + - - {/* */} + Dialog Title + Dialog Content - - oi ); }; From be135360686d871de44429aea91daee6896da3ca Mon Sep 17 00:00:00 2001 From: Bernardo Sunderhus Date: Tue, 12 Jul 2022 21:17:19 +0000 Subject: [PATCH 09/17] Adds styles for DialogTitle --- .../src/components/Dialog/Dialog.types.ts | 6 +- .../src/components/Dialog/renderDialog.tsx | 21 ++++--- .../src/components/Dialog/useDialog.ts | 63 +++++++++---------- .../Dialog/useDialogContextValues.ts | 7 ++- .../DialogContent/DialogContent.tsx | 4 +- .../DialogContent/DialogContent.types.ts | 5 ++ .../DialogContent/renderDialogContent.tsx | 11 +++- .../useDialogContentContextValues.ts | 12 ++++ .../DialogContent/useDialogContentStyles.ts | 5 ++ .../components/DialogTitle/useDialogTitle.tsx | 1 + .../DialogTitle/useDialogTitleStyles.ts | 55 +++++++++++----- .../DialogTrigger/useDialogTrigger.ts | 6 +- .../react-dialog/src/contexts/constants.ts | 1 + .../src/contexts/dialogContentContext.ts | 12 ++++ .../stories/Dialog/DialogDefault.stories.tsx | 6 +- .../stories/Dialog/DialogNested.stories.tsx | 32 ++++++++++ .../DialogNoFocusableElement.stories.tsx | 31 +++++++++ .../stories/Dialog/DialogNonModal.stories.tsx | 23 +++++++ .../src/stories/Dialog/index.stories.tsx | 3 + .../src/utils/normalizeDefaultPrevented.ts | 11 ++++ .../src/utils/normalizeSetStateAction.ts | 8 +++ 21 files changed, 255 insertions(+), 68 deletions(-) create mode 100644 packages/react-components/react-dialog/src/components/DialogContent/useDialogContentContextValues.ts create mode 100644 packages/react-components/react-dialog/src/contexts/constants.ts create mode 100644 packages/react-components/react-dialog/src/contexts/dialogContentContext.ts create mode 100644 packages/react-components/react-dialog/src/stories/Dialog/DialogNested.stories.tsx create mode 100644 packages/react-components/react-dialog/src/stories/Dialog/DialogNoFocusableElement.stories.tsx create mode 100644 packages/react-components/react-dialog/src/stories/Dialog/DialogNonModal.stories.tsx create mode 100644 packages/react-components/react-dialog/src/utils/normalizeDefaultPrevented.ts create mode 100644 packages/react-components/react-dialog/src/utils/normalizeSetStateAction.ts 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 dbe5334cf4928..17993cb69d9ea 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 @@ -1,6 +1,7 @@ import type * as React from 'react'; import type { ComponentProps, ComponentState, Slot } from '@fluentui/react-utilities'; import type { DialogContextValue } from '../../contexts/dialogContext'; +import { DialogContentContextValue } from '../../contexts/dialogContentContext'; export type DialogSlots = { /** @@ -26,8 +27,11 @@ export type DialogModalType = 'modal' | 'non-modal' | 'alert'; export type DialogContextValues = { dialog: DialogContextValue; + dialogContent: DialogContentContextValue; }; +export type DialogOnOpenChange = (...args: DialogOpenChangeArgs) => void; + export type DialogProps = ComponentProps> & { /** * Dialog variations. @@ -62,7 +66,7 @@ export type DialogProps = ComponentProps> & { * Callback fired when the component changes value from open state. * @default undefined */ - onOpenChange?(...args: DialogOpenChangeArgs): void; + onOpenChange?: DialogOnOpenChange; /** * Can contain two children including {@link DialogTrigger} and {@link DialogContent}. * Alternatively can only contain {@link DialogContent} if using trigger outside dialog, or controlling state. diff --git a/packages/react-components/react-dialog/src/components/Dialog/renderDialog.tsx b/packages/react-components/react-dialog/src/components/Dialog/renderDialog.tsx index e4f6736f42314..720c3aefc1a4d 100644 --- a/packages/react-components/react-dialog/src/components/Dialog/renderDialog.tsx +++ b/packages/react-components/react-dialog/src/components/Dialog/renderDialog.tsx @@ -3,6 +3,7 @@ import { getSlots } from '@fluentui/react-utilities'; import { Portal } from '@fluentui/react-portal'; import type { DialogState, DialogSlots, DialogContextValues } from './Dialog.types'; import { DialogProvider } from '../../contexts/dialogContext'; +import { DialogContentProvider } from '../../contexts/dialogContentContext'; /** * Render the final JSX of Dialog @@ -13,15 +14,17 @@ export const renderDialog_unstable = (state: DialogState, contextValues: DialogC return ( - {trigger} - {open && ( - - - {slots.overlay && } - {content} - - - )} + + {trigger} + {open && ( + + + {slots.overlay && } + {content} + + + )} + ); }; 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 07fbe4a773171..ccb24f5c0ed3e 100644 --- a/packages/react-components/react-dialog/src/components/Dialog/useDialog.ts +++ b/packages/react-components/react-dialog/src/components/Dialog/useDialog.ts @@ -10,6 +10,8 @@ import { DialogRequestOpenChangeData } from '../../contexts/dialogContext'; import { Escape } from '@fluentui/keyboard-keys'; import { useFocusFinders } from '@fluentui/react-tabster'; import { useFluent_unstable } from '@fluentui/react-shared-contexts'; +import { normalizeDefaultPrevented } from '../../utils/normalizeDefaultPrevented'; +import { normalizeSetStateAction } from '../../utils/normalizeSetStateAction'; /** * Create the state required to render Dialog. @@ -62,7 +64,11 @@ export const useDialog_unstable = (props: DialogProps, ref: React.Ref) => { overlayShorthand?.onClick?.(event); @@ -75,6 +81,7 @@ export const useDialog_unstable = (props: DialogProps, ref: React.Ref): (prev: boolean) => boolean { - return typeof setOpen === 'function' ? setOpen : () => setOpen; -} - -/** - * normalizes defaultPrevented to work the same way between synthetic events and regular event - */ -function normalizeDefaultPrevented(event: React.SyntheticEvent | Event) { - if (event instanceof Event) { - return () => event.defaultPrevented; - } - return event.isDefaultPrevented; -} - /** * Checks if keydown event is a proper Escape key dismiss */ @@ -191,22 +181,27 @@ function useFocusFirstElement({ console.warn('A Dialog should have at least one focusable element inside DialogContent'); } - // if (triggerRef.current && targetDocument) { - // const trigger = triggerRef.current; - // trigger.blur(); - // const listener = (event: KeyboardEvent) => { - // if (isEscapeKeyDismiss(event, modalType)) { - // requestOpenChange({ - // event, - // open: false, - // type: 'documentEscapeKeyDown', - // }); - // trigger.focus(); - // } - // }; - // targetDocument.addEventListener('keydown', listener); - // return () => targetDocument.removeEventListener('keydown', listener); - // } + if (triggerRef.current && targetDocument) { + const trigger = triggerRef.current; + if (targetDocument.activeElement === trigger) { + trigger.blur(); + } + const listener = (event: KeyboardEvent) => { + if (isEscapeKeyDismiss(event, modalType)) { + requestOpenChange({ + event, + open: false, + type: 'documentEscapeKeyDown', + }); + trigger.focus(); + event.stopImmediatePropagation(); + } + }; + targetDocument.addEventListener('keydown', listener, { passive: false }); + return () => { + targetDocument.removeEventListener('keydown', listener); + }; + } } }, [findFirstFocusable, requestOpenChange, open, modalType, targetDocument]); 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 f5742e1b62153..ebbf927c54850 100644 --- a/packages/react-components/react-dialog/src/components/Dialog/useDialogContextValues.ts +++ b/packages/react-components/react-dialog/src/components/Dialog/useDialogContextValues.ts @@ -1,3 +1,4 @@ +import { DialogContentContextValue } from '../../contexts/dialogContentContext'; import { DialogContextValue } from '../../contexts/dialogContext'; import type { DialogContextValues, DialogState } from './Dialog.types'; @@ -16,5 +17,9 @@ export function useDialogContextValues_unstable(state: DialogState): DialogConte contentRef, }; - return { dialog }; + return { dialog, dialogContent: defaultDialogContentContextValue }; } + +const defaultDialogContentContextValue: DialogContentContextValue = { + isInsideDialogContent: false, +}; diff --git a/packages/react-components/react-dialog/src/components/DialogContent/DialogContent.tsx b/packages/react-components/react-dialog/src/components/DialogContent/DialogContent.tsx index 474add2c8109a..be9addece3248 100644 --- a/packages/react-components/react-dialog/src/components/DialogContent/DialogContent.tsx +++ b/packages/react-components/react-dialog/src/components/DialogContent/DialogContent.tsx @@ -4,6 +4,7 @@ import { renderDialogContent_unstable } from './renderDialogContent'; import { useDialogContentStyles_unstable } from './useDialogContentStyles'; import type { DialogContentProps } from './DialogContent.types'; import type { ForwardRefComponent } from '@fluentui/react-utilities'; +import { useDialogContextValues_unstable } from './useDialogContentContextValues'; /** * DialogContent component represents the visual part of a `Dialog` as a whole, @@ -13,7 +14,8 @@ export const DialogContent: ForwardRefComponent = React.forw const state = useDialogContent_unstable(props, ref); useDialogContentStyles_unstable(state); - return renderDialogContent_unstable(state); + const contextValues = useDialogContextValues_unstable(state); + return renderDialogContent_unstable(state, contextValues); }); DialogContent.displayName = 'DialogContent'; diff --git a/packages/react-components/react-dialog/src/components/DialogContent/DialogContent.types.ts b/packages/react-components/react-dialog/src/components/DialogContent/DialogContent.types.ts index 3142ada984df3..1c77c19b68a89 100644 --- a/packages/react-components/react-dialog/src/components/DialogContent/DialogContent.types.ts +++ b/packages/react-components/react-dialog/src/components/DialogContent/DialogContent.types.ts @@ -1,9 +1,14 @@ import type { ComponentProps, ComponentState, Slot } from '@fluentui/react-utilities'; +import { DialogContentContextValue } from '../../contexts/dialogContentContext'; export type DialogContentSlots = { root: Slot<'div', 'main'>; }; +export type DialogContentContextValues = { + dialogContent: DialogContentContextValue; +}; + /** * DialogContent Props */ diff --git a/packages/react-components/react-dialog/src/components/DialogContent/renderDialogContent.tsx b/packages/react-components/react-dialog/src/components/DialogContent/renderDialogContent.tsx index 56ef46582797d..77b97b0c354e1 100644 --- a/packages/react-components/react-dialog/src/components/DialogContent/renderDialogContent.tsx +++ b/packages/react-components/react-dialog/src/components/DialogContent/renderDialogContent.tsx @@ -1,12 +1,17 @@ import * as React from 'react'; import { getSlots } from '@fluentui/react-utilities'; -import type { DialogContentState, DialogContentSlots } from './DialogContent.types'; +import type { DialogContentState, DialogContentSlots, DialogContentContextValues } from './DialogContent.types'; +import { DialogContentProvider } from '../../contexts/dialogContentContext'; /** * Render the final JSX of DialogContent */ -export const renderDialogContent_unstable = (state: DialogContentState) => { +export const renderDialogContent_unstable = (state: DialogContentState, contextValues: DialogContentContextValues) => { const { slots, slotProps } = getSlots(state); - return ; + return ( + + + + ); }; diff --git a/packages/react-components/react-dialog/src/components/DialogContent/useDialogContentContextValues.ts b/packages/react-components/react-dialog/src/components/DialogContent/useDialogContentContextValues.ts new file mode 100644 index 0000000000000..babfe2d368327 --- /dev/null +++ b/packages/react-components/react-dialog/src/components/DialogContent/useDialogContentContextValues.ts @@ -0,0 +1,12 @@ +import { DialogContentContextValue } from '../../contexts/dialogContentContext'; +import type { DialogContentState, DialogContentContextValues } from './DialogContent.types'; + +const defaultDialogContentContextValue: DialogContentContextValue = { + isInsideDialogContent: true, +}; + +export function useDialogContextValues_unstable(state: DialogContentState): DialogContentContextValues { + return { + dialogContent: defaultDialogContentContextValue, + }; +} diff --git a/packages/react-components/react-dialog/src/components/DialogContent/useDialogContentStyles.ts b/packages/react-components/react-dialog/src/components/DialogContent/useDialogContentStyles.ts index 0089e5dc7f0ba..f74e69fca7a24 100644 --- a/packages/react-components/react-dialog/src/components/DialogContent/useDialogContentStyles.ts +++ b/packages/react-components/react-dialog/src/components/DialogContent/useDialogContentStyles.ts @@ -2,6 +2,7 @@ import { makeStyles, mergeClasses, shorthands } from '@griffel/react'; import type { DialogContentSlots, DialogContentState } from './DialogContent.types'; import type { SlotClassNames } from '@fluentui/react-utilities'; import { tokens } from '@fluentui/react-theme'; +import { MEDIA_QUERY_BREAKPOINT_SELECTOR } from '../../contexts/constants'; export const dialogContentClassNames: SlotClassNames = { root: 'fui-DialogContent', @@ -21,6 +22,10 @@ const useStyles = makeStyles({ maxHeight: '100vh', boxShadow: tokens.shadow64, backgroundColor: tokens.colorNeutralBackground1, + [MEDIA_QUERY_BREAKPOINT_SELECTOR]: { + maxWidth: '100vw', + width: '100%', + }, ...shorthands.borderRadius('8px'), ...shorthands.margin('auto'), }, diff --git a/packages/react-components/react-dialog/src/components/DialogTitle/useDialogTitle.tsx b/packages/react-components/react-dialog/src/components/DialogTitle/useDialogTitle.tsx index df38196d7922b..31ddc0f5ec388 100644 --- a/packages/react-components/react-dialog/src/components/DialogTitle/useDialogTitle.tsx +++ b/packages/react-components/react-dialog/src/components/DialogTitle/useDialogTitle.tsx @@ -31,6 +31,7 @@ export const useDialogTitle_unstable = (props: DialogTitleProps, ref: React.Ref< required: modalType === 'non-modal', defaultProps: { type: 'button', // This is added because the default for type is 'submit' + 'aria-label': 'close', children: , }, }), diff --git a/packages/react-components/react-dialog/src/components/DialogTitle/useDialogTitleStyles.ts b/packages/react-components/react-dialog/src/components/DialogTitle/useDialogTitleStyles.ts index 826e0ca32ee14..6f71e7a5e4d6e 100644 --- a/packages/react-components/react-dialog/src/components/DialogTitle/useDialogTitleStyles.ts +++ b/packages/react-components/react-dialog/src/components/DialogTitle/useDialogTitleStyles.ts @@ -2,6 +2,7 @@ import { makeStyles, mergeClasses, shorthands } from '@griffel/react'; import type { DialogTitleSlots, DialogTitleState } from './DialogTitle.types'; import type { SlotClassNames } from '@fluentui/react-utilities'; import { createFocusOutlineStyle } from '@fluentui/react-tabster'; +import { typographyStyles } from '@fluentui/react-theme'; export const dialogTitleClassNames: SlotClassNames = { root: 'fui-DialogTitle', @@ -12,21 +13,37 @@ export const dialogTitleClassNames: SlotClassNames = { * Styles for the root slot */ const useStyles = makeStyles({ - root: {}, - closeButton: {}, - focusIndicator: createFocusOutlineStyle(), + root: { + display: 'flex', + alignItems: 'start', + columnGap: '8px', + justifyContent: 'space-between', + ...typographyStyles.subtitle1, + }, + rootWithoutCloseButton: { + ...shorthands.padding('24px', '24px', '8px', '24px'), + }, + rootWithCloseButton: { + ...shorthands.padding('24px', '20px', '8px', '24px'), + }, + closeButton: { + position: 'relative', + lineHeight: '0', + }, + closeButtonFocusIndicator: createFocusOutlineStyle(), // TODO: this should be extracted to another package resetButton: { - // boxSizing: 'content-box', - // backgroundColor: 'inherit', - // color: 'inherit', - // fontFamily: 'inherit', - // fontSize: 'inherit', - // lineHeight: 'normal', - // ...shorthands.overflow('visible'), - // ...shorthands.padding(0), - // WebkitAppearance: 'button', - // textAlign: 'unset', + boxSizing: 'content-box', + backgroundColor: 'inherit', + color: 'inherit', + fontFamily: 'inherit', + fontSize: 'inherit', + lineHeight: 'normal', + ...shorthands.overflow('visible'), + ...shorthands.padding(0), + ...shorthands.borderStyle('none'), + WebkitAppearance: 'button', + textAlign: 'unset', }, }); @@ -35,13 +52,19 @@ const useStyles = makeStyles({ */ export const useDialogTitleStyles_unstable = (state: DialogTitleState): DialogTitleState => { const styles = useStyles(); - state.root.className = mergeClasses(dialogTitleClassNames.root, styles.root, state.root.className); + state.root.className = mergeClasses( + dialogTitleClassNames.root, + styles.root, + state.closeButton && styles.rootWithCloseButton, + !state.closeButton && styles.rootWithoutCloseButton, + state.root.className, + ); if (state.closeButton) { state.closeButton.className = mergeClasses( dialogTitleClassNames.closeButton, - styles.closeButton, styles.resetButton, - styles.focusIndicator, + styles.closeButton, + styles.closeButtonFocusIndicator, state.closeButton.className, ); } diff --git a/packages/react-components/react-dialog/src/components/DialogTrigger/useDialogTrigger.ts b/packages/react-components/react-dialog/src/components/DialogTrigger/useDialogTrigger.ts index 3be190daa4f3c..e8663a29ff91c 100644 --- a/packages/react-components/react-dialog/src/components/DialogTrigger/useDialogTrigger.ts +++ b/packages/react-components/react-dialog/src/components/DialogTrigger/useDialogTrigger.ts @@ -6,6 +6,7 @@ import { useMergedRefs, } from '@fluentui/react-utilities'; import * as React from 'react'; +import { useDialogContentContext_unstable } from '../../contexts/dialogContentContext'; import { useDialogContext_unstable } from '../../contexts/dialogContext'; import { DialogTriggerChildProps, @@ -27,6 +28,8 @@ export const useDialogTrigger_unstable = (props: DialogTriggerProps): DialogTrig const requestOpenChange = useDialogContext_unstable(ctx => ctx.requestOpenChange); const triggerRef = useDialogContext_unstable(ctx => ctx.triggerRef); + const { isInsideDialogContent } = useDialogContentContext_unstable(); + const { triggerAttributes } = useModalAttributes(); const handleClick = useEventCallback((event: React.MouseEvent) => { @@ -43,7 +46,8 @@ export const useDialogTrigger_unstable = (props: DialogTriggerProps): DialogTrig return { children: applyTriggerPropsToChildren(children, { 'aria-haspopup': 'dialog', - ref: useMergedRefs(child?.ref, triggerRef), + // NOTE: if trigger is inside DialogContent, then do not merge ref + ref: useMergedRefs(...(isInsideDialogContent ? [child?.ref] : [child?.ref, triggerRef])), onClick: handleClick, ...triggerAttributes, }), diff --git a/packages/react-components/react-dialog/src/contexts/constants.ts b/packages/react-components/react-dialog/src/contexts/constants.ts new file mode 100644 index 0000000000000..37af304dcb2d3 --- /dev/null +++ b/packages/react-components/react-dialog/src/contexts/constants.ts @@ -0,0 +1 @@ +export const MEDIA_QUERY_BREAKPOINT_SELECTOR = '@media screen and (max-width: 480px)'; diff --git a/packages/react-components/react-dialog/src/contexts/dialogContentContext.ts b/packages/react-components/react-dialog/src/contexts/dialogContentContext.ts new file mode 100644 index 0000000000000..9c4aaad7c319d --- /dev/null +++ b/packages/react-components/react-dialog/src/contexts/dialogContentContext.ts @@ -0,0 +1,12 @@ +import { createContext, useContext } from 'react'; + +export type DialogContentContextValue = { + isInsideDialogContent: boolean; +}; + +export const DialogContentContext = createContext({ + isInsideDialogContent: false, +}); + +export const DialogContentProvider = DialogContentContext.Provider; +export const useDialogContentContext_unstable = () => useContext(DialogContentContext); diff --git a/packages/react-components/react-dialog/src/stories/Dialog/DialogDefault.stories.tsx b/packages/react-components/react-dialog/src/stories/Dialog/DialogDefault.stories.tsx index 97a948c59a32b..70bfc8269c63d 100644 --- a/packages/react-components/react-dialog/src/stories/Dialog/DialogDefault.stories.tsx +++ b/packages/react-components/react-dialog/src/stories/Dialog/DialogDefault.stories.tsx @@ -6,14 +6,16 @@ import { Button } from '@fluentui/react-components'; export const Default = (props: Partial) => { return ( <> - + Dialog Title - Dialog Content + + + diff --git a/packages/react-components/react-dialog/src/stories/Dialog/DialogNested.stories.tsx b/packages/react-components/react-dialog/src/stories/Dialog/DialogNested.stories.tsx new file mode 100644 index 0000000000000..a86cf0054b07e --- /dev/null +++ b/packages/react-components/react-dialog/src/stories/Dialog/DialogNested.stories.tsx @@ -0,0 +1,32 @@ +import * as React from 'react'; +import { Dialog, DialogTrigger, DialogContent, DialogTitle } from '@fluentui/react-dialog'; +import type { DialogProps } from '@fluentui/react-dialog'; +import { Button } from '@fluentui/react-components'; + +export const Nested = (props: Partial) => { + return ( + <> + + + + + + Dialog Title + Dialog Content + + + + + + Inner Dialog Title + Dialog Content + {/* + + */} + + + + + + ); +}; diff --git a/packages/react-components/react-dialog/src/stories/Dialog/DialogNoFocusableElement.stories.tsx b/packages/react-components/react-dialog/src/stories/Dialog/DialogNoFocusableElement.stories.tsx new file mode 100644 index 0000000000000..d10ff3ab586c4 --- /dev/null +++ b/packages/react-components/react-dialog/src/stories/Dialog/DialogNoFocusableElement.stories.tsx @@ -0,0 +1,31 @@ +import * as React from 'react'; +import { Dialog, DialogTrigger, DialogContent, DialogTitle } from '@fluentui/react-dialog'; +import type { DialogProps } from '@fluentui/react-dialog'; +import { Button } from '@fluentui/react-components'; + +export const NoFocusableElement = (props: Partial) => { + return ( + <> + + + + + + Dialog Title +

⚠️A Dialog without focusable elements is not recommended!⚠️

+

Escape key and overlay click still works to ensure this modal can be closed

+
+
+ + + + + + Dialog Title +

⚠️A Dialog without focusable elements is not recommended!⚠️

+

Escape key still works to ensure this modal can be closed

+
+
+ + ); +}; diff --git a/packages/react-components/react-dialog/src/stories/Dialog/DialogNonModal.stories.tsx b/packages/react-components/react-dialog/src/stories/Dialog/DialogNonModal.stories.tsx new file mode 100644 index 0000000000000..b9438fa068e2e --- /dev/null +++ b/packages/react-components/react-dialog/src/stories/Dialog/DialogNonModal.stories.tsx @@ -0,0 +1,23 @@ +import * as React from 'react'; +import { Dialog, DialogTrigger, DialogContent, DialogTitle } from '@fluentui/react-dialog'; +import type { DialogProps } from '@fluentui/react-dialog'; +import { Button } from '@fluentui/react-components'; + +export const NonModal = (props: Partial) => { + return ( + <> + + + + + + Dialog Title + Dialog Content + + + + + + + ); +}; diff --git a/packages/react-components/react-dialog/src/stories/Dialog/index.stories.tsx b/packages/react-components/react-dialog/src/stories/Dialog/index.stories.tsx index 34ebfa88e1c52..b8976a4692fc4 100644 --- a/packages/react-components/react-dialog/src/stories/Dialog/index.stories.tsx +++ b/packages/react-components/react-dialog/src/stories/Dialog/index.stories.tsx @@ -4,6 +4,9 @@ import descriptionMd from './DialogDescription.md'; import bestPracticesMd from './DialogBestPractices.md'; export { Default } from './DialogDefault.stories'; +export { NonModal } from './DialogNonModal.stories'; +export { Nested } from './DialogNested.stories'; +export { NoFocusableElement } from './DialogNoFocusableElement.stories'; export default { title: 'Components/Dialog', diff --git a/packages/react-components/react-dialog/src/utils/normalizeDefaultPrevented.ts b/packages/react-components/react-dialog/src/utils/normalizeDefaultPrevented.ts new file mode 100644 index 0000000000000..33e1851da1881 --- /dev/null +++ b/packages/react-components/react-dialog/src/utils/normalizeDefaultPrevented.ts @@ -0,0 +1,11 @@ +import * as React from 'react'; + +/** + * normalizes defaultPrevented to work the same way between synthetic events and regular event + */ +export function normalizeDefaultPrevented(event: React.SyntheticEvent | Event) { + if (event instanceof Event) { + return () => event.defaultPrevented; + } + return event.isDefaultPrevented; +} diff --git a/packages/react-components/react-dialog/src/utils/normalizeSetStateAction.ts b/packages/react-components/react-dialog/src/utils/normalizeSetStateAction.ts new file mode 100644 index 0000000000000..f12710a3fbdfe --- /dev/null +++ b/packages/react-components/react-dialog/src/utils/normalizeSetStateAction.ts @@ -0,0 +1,8 @@ +import * as React from 'react'; + +/** + * Normalizes a set state action into a setter function + */ +export function normalizeSetStateAction(setOpen: React.SetStateAction): (prev: T) => T { + return typeof setOpen === 'function' ? (setOpen as (prevState: T) => T) : () => setOpen; +} From 30f033b959dbf45164d3628c7128672df7ed9bcc Mon Sep 17 00:00:00 2001 From: Bernardo Sunderhus Date: Tue, 12 Jul 2022 22:31:13 +0000 Subject: [PATCH 10/17] Modifies triggerRef --- .../src/components/Dialog/Dialog.types.ts | 2 -- .../src/components/Dialog/renderDialog.tsx | 21 ++++++++----------- .../src/components/Dialog/useDialog.ts | 13 ++++++++++++ .../Dialog/useDialogContextValues.ts | 7 +------ .../src/components/Dialog/useDialogStyles.ts | 18 ++++------------ .../DialogContent/DialogContent.tsx | 4 +--- .../DialogContent/DialogContent.types.ts | 5 ----- .../DialogContent/renderDialogContent.tsx | 11 +++------- .../useDialogContentContextValues.ts | 12 ----------- .../DialogContent/useDialogContentStyles.ts | 5 ++++- .../DialogTrigger/useDialogTrigger.ts | 13 ++---------- .../src/contexts/dialogContentContext.ts | 12 ----------- .../src/contexts/dialogContext.ts | 8 +++++-- 13 files changed, 43 insertions(+), 88 deletions(-) delete mode 100644 packages/react-components/react-dialog/src/components/DialogContent/useDialogContentContextValues.ts delete mode 100644 packages/react-components/react-dialog/src/contexts/dialogContentContext.ts 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 17993cb69d9ea..d590602e9fa3d 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 @@ -1,7 +1,6 @@ import type * as React from 'react'; import type { ComponentProps, ComponentState, Slot } from '@fluentui/react-utilities'; import type { DialogContextValue } from '../../contexts/dialogContext'; -import { DialogContentContextValue } from '../../contexts/dialogContentContext'; export type DialogSlots = { /** @@ -27,7 +26,6 @@ export type DialogModalType = 'modal' | 'non-modal' | 'alert'; export type DialogContextValues = { dialog: DialogContextValue; - dialogContent: DialogContentContextValue; }; export type DialogOnOpenChange = (...args: DialogOpenChangeArgs) => void; diff --git a/packages/react-components/react-dialog/src/components/Dialog/renderDialog.tsx b/packages/react-components/react-dialog/src/components/Dialog/renderDialog.tsx index 720c3aefc1a4d..e4f6736f42314 100644 --- a/packages/react-components/react-dialog/src/components/Dialog/renderDialog.tsx +++ b/packages/react-components/react-dialog/src/components/Dialog/renderDialog.tsx @@ -3,7 +3,6 @@ import { getSlots } from '@fluentui/react-utilities'; import { Portal } from '@fluentui/react-portal'; import type { DialogState, DialogSlots, DialogContextValues } from './Dialog.types'; import { DialogProvider } from '../../contexts/dialogContext'; -import { DialogContentProvider } from '../../contexts/dialogContentContext'; /** * Render the final JSX of Dialog @@ -14,17 +13,15 @@ export const renderDialog_unstable = (state: DialogState, contextValues: DialogC return ( - - {trigger} - {open && ( - - - {slots.overlay && } - {content} - - - )} - + {trigger} + {open && ( + + + {slots.overlay && } + {content} + + + )} ); }; 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 ccb24f5c0ed3e..a43f946c9b638 100644 --- a/packages/react-components/react-dialog/src/components/Dialog/useDialog.ts +++ b/packages/react-components/react-dialog/src/components/Dialog/useDialog.ts @@ -60,6 +60,11 @@ export const useDialog_unstable = (props: DialogProps, ref: React.Ref).current = isOpening(open, getNextOpen) + ? (data.event.currentTarget as HTMLElement) + : null; + // updates value setOpen(getNextOpen); } }); @@ -153,6 +158,13 @@ function isOverlayClickDismiss(event: React.MouseEvent, type: DialogModalType): return type === 'modal' && !isDefaultPrevented(); } +/** + * Checks if dialog is opening + */ +function isOpening(current: boolean, next: Extract, Function>) { + return !current && next(current) === true; +} + /** * Focus first element on content when dialog is opened, * in case there's no focusable element, then a eventlistener is added to document @@ -173,6 +185,7 @@ function useFocusFirstElement({ const element = contentRef.current && findFirstFocusable(contentRef.current); if (element) { element.focus(); + // NOTE: if it's non-modal global listener to escape is necessary if (modalType !== 'non-modal') { return; } 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 ebbf927c54850..f5742e1b62153 100644 --- a/packages/react-components/react-dialog/src/components/Dialog/useDialogContextValues.ts +++ b/packages/react-components/react-dialog/src/components/Dialog/useDialogContextValues.ts @@ -1,4 +1,3 @@ -import { DialogContentContextValue } from '../../contexts/dialogContentContext'; import { DialogContextValue } from '../../contexts/dialogContext'; import type { DialogContextValues, DialogState } from './Dialog.types'; @@ -17,9 +16,5 @@ export function useDialogContextValues_unstable(state: DialogState): DialogConte contentRef, }; - return { dialog, dialogContent: defaultDialogContentContextValue }; + return { dialog }; } - -const defaultDialogContentContextValue: DialogContentContextValue = { - isInsideDialogContent: false, -}; diff --git a/packages/react-components/react-dialog/src/components/Dialog/useDialogStyles.ts b/packages/react-components/react-dialog/src/components/Dialog/useDialogStyles.ts index d3a4f2bb5ef7f..70d211cb37e99 100644 --- a/packages/react-components/react-dialog/src/components/Dialog/useDialogStyles.ts +++ b/packages/react-components/react-dialog/src/components/Dialog/useDialogStyles.ts @@ -1,5 +1,5 @@ import type { SlotClassNames } from '@fluentui/react-utilities'; -import { makeStyles, mergeClasses } from '@griffel/react'; +import { makeStyles, mergeClasses, shorthands } from '@griffel/react'; import type { DialogSlots, DialogState } from './Dialog.types'; export const dialogClassNames: SlotClassNames = { @@ -10,21 +10,11 @@ export const dialogClassNames: SlotClassNames = { * Styles for the root slot */ const useStyles = makeStyles({ - root: { - position: 'fixed', - width: '100%', - height: '100%', - top: 0, - left: 0, - display: 'flex', - justifyContent: 'center', - alignItems: 'center', - }, + root: {}, overlay: { - position: 'absolute', - height: '100%', - width: '100%', + position: 'fixed', backgroundColor: 'rgba(0, 0, 0, 0.4)', + ...shorthands.inset('0px'), }, }); diff --git a/packages/react-components/react-dialog/src/components/DialogContent/DialogContent.tsx b/packages/react-components/react-dialog/src/components/DialogContent/DialogContent.tsx index be9addece3248..474add2c8109a 100644 --- a/packages/react-components/react-dialog/src/components/DialogContent/DialogContent.tsx +++ b/packages/react-components/react-dialog/src/components/DialogContent/DialogContent.tsx @@ -4,7 +4,6 @@ import { renderDialogContent_unstable } from './renderDialogContent'; import { useDialogContentStyles_unstable } from './useDialogContentStyles'; import type { DialogContentProps } from './DialogContent.types'; import type { ForwardRefComponent } from '@fluentui/react-utilities'; -import { useDialogContextValues_unstable } from './useDialogContentContextValues'; /** * DialogContent component represents the visual part of a `Dialog` as a whole, @@ -14,8 +13,7 @@ export const DialogContent: ForwardRefComponent = React.forw const state = useDialogContent_unstable(props, ref); useDialogContentStyles_unstable(state); - const contextValues = useDialogContextValues_unstable(state); - return renderDialogContent_unstable(state, contextValues); + return renderDialogContent_unstable(state); }); DialogContent.displayName = 'DialogContent'; diff --git a/packages/react-components/react-dialog/src/components/DialogContent/DialogContent.types.ts b/packages/react-components/react-dialog/src/components/DialogContent/DialogContent.types.ts index 1c77c19b68a89..3142ada984df3 100644 --- a/packages/react-components/react-dialog/src/components/DialogContent/DialogContent.types.ts +++ b/packages/react-components/react-dialog/src/components/DialogContent/DialogContent.types.ts @@ -1,14 +1,9 @@ import type { ComponentProps, ComponentState, Slot } from '@fluentui/react-utilities'; -import { DialogContentContextValue } from '../../contexts/dialogContentContext'; export type DialogContentSlots = { root: Slot<'div', 'main'>; }; -export type DialogContentContextValues = { - dialogContent: DialogContentContextValue; -}; - /** * DialogContent Props */ diff --git a/packages/react-components/react-dialog/src/components/DialogContent/renderDialogContent.tsx b/packages/react-components/react-dialog/src/components/DialogContent/renderDialogContent.tsx index 77b97b0c354e1..56ef46582797d 100644 --- a/packages/react-components/react-dialog/src/components/DialogContent/renderDialogContent.tsx +++ b/packages/react-components/react-dialog/src/components/DialogContent/renderDialogContent.tsx @@ -1,17 +1,12 @@ import * as React from 'react'; import { getSlots } from '@fluentui/react-utilities'; -import type { DialogContentState, DialogContentSlots, DialogContentContextValues } from './DialogContent.types'; -import { DialogContentProvider } from '../../contexts/dialogContentContext'; +import type { DialogContentState, DialogContentSlots } from './DialogContent.types'; /** * Render the final JSX of DialogContent */ -export const renderDialogContent_unstable = (state: DialogContentState, contextValues: DialogContentContextValues) => { +export const renderDialogContent_unstable = (state: DialogContentState) => { const { slots, slotProps } = getSlots(state); - return ( - - - - ); + return ; }; diff --git a/packages/react-components/react-dialog/src/components/DialogContent/useDialogContentContextValues.ts b/packages/react-components/react-dialog/src/components/DialogContent/useDialogContentContextValues.ts deleted file mode 100644 index babfe2d368327..0000000000000 --- a/packages/react-components/react-dialog/src/components/DialogContent/useDialogContentContextValues.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { DialogContentContextValue } from '../../contexts/dialogContentContext'; -import type { DialogContentState, DialogContentContextValues } from './DialogContent.types'; - -const defaultDialogContentContextValue: DialogContentContextValue = { - isInsideDialogContent: true, -}; - -export function useDialogContextValues_unstable(state: DialogContentState): DialogContentContextValues { - return { - dialogContent: defaultDialogContentContextValue, - }; -} diff --git a/packages/react-components/react-dialog/src/components/DialogContent/useDialogContentStyles.ts b/packages/react-components/react-dialog/src/components/DialogContent/useDialogContentStyles.ts index f74e69fca7a24..839a02c9923c3 100644 --- a/packages/react-components/react-dialog/src/components/DialogContent/useDialogContentStyles.ts +++ b/packages/react-components/react-dialog/src/components/DialogContent/useDialogContentStyles.ts @@ -13,7 +13,10 @@ export const dialogContentClassNames: SlotClassNames = { */ const useStyles = makeStyles({ root: { - position: 'relative', + position: 'fixed', + top: '50%', + left: '50%', + transform: 'translate(-50%, -50%)', display: 'flex', flexDirection: 'column', width: 'fit-content', diff --git a/packages/react-components/react-dialog/src/components/DialogTrigger/useDialogTrigger.ts b/packages/react-components/react-dialog/src/components/DialogTrigger/useDialogTrigger.ts index e8663a29ff91c..3b0b4419525bd 100644 --- a/packages/react-components/react-dialog/src/components/DialogTrigger/useDialogTrigger.ts +++ b/packages/react-components/react-dialog/src/components/DialogTrigger/useDialogTrigger.ts @@ -1,12 +1,6 @@ import { useModalAttributes } from '@fluentui/react-tabster'; -import { - applyTriggerPropsToChildren, - getTriggerChild, - useEventCallback, - useMergedRefs, -} from '@fluentui/react-utilities'; +import { applyTriggerPropsToChildren, getTriggerChild, useEventCallback } from '@fluentui/react-utilities'; import * as React from 'react'; -import { useDialogContentContext_unstable } from '../../contexts/dialogContentContext'; import { useDialogContext_unstable } from '../../contexts/dialogContext'; import { DialogTriggerChildProps, @@ -27,8 +21,6 @@ export const useDialogTrigger_unstable = (props: DialogTriggerProps): DialogTrig const child = React.isValidElement(children) ? getTriggerChild(children) : undefined; const requestOpenChange = useDialogContext_unstable(ctx => ctx.requestOpenChange); - const triggerRef = useDialogContext_unstable(ctx => ctx.triggerRef); - const { isInsideDialogContent } = useDialogContentContext_unstable(); const { triggerAttributes } = useModalAttributes(); @@ -46,8 +38,7 @@ export const useDialogTrigger_unstable = (props: DialogTriggerProps): DialogTrig return { children: applyTriggerPropsToChildren(children, { 'aria-haspopup': 'dialog', - // NOTE: if trigger is inside DialogContent, then do not merge ref - ref: useMergedRefs(...(isInsideDialogContent ? [child?.ref] : [child?.ref, triggerRef])), + ref: child?.ref as React.Ref, onClick: handleClick, ...triggerAttributes, }), diff --git a/packages/react-components/react-dialog/src/contexts/dialogContentContext.ts b/packages/react-components/react-dialog/src/contexts/dialogContentContext.ts deleted file mode 100644 index 9c4aaad7c319d..0000000000000 --- a/packages/react-components/react-dialog/src/contexts/dialogContentContext.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { createContext, useContext } from 'react'; - -export type DialogContentContextValue = { - isInsideDialogContent: boolean; -}; - -export const DialogContentContext = createContext({ - isInsideDialogContent: false, -}); - -export const DialogContentProvider = DialogContentContext.Provider; -export const useDialogContentContext_unstable = () => useContext(DialogContentContext); diff --git a/packages/react-components/react-dialog/src/contexts/dialogContext.ts b/packages/react-components/react-dialog/src/contexts/dialogContext.ts index 5a8ea61d00add..bd301215454ef 100644 --- a/packages/react-components/react-dialog/src/contexts/dialogContext.ts +++ b/packages/react-components/react-dialog/src/contexts/dialogContext.ts @@ -9,8 +9,12 @@ export type DialogRequestOpenChangeData = { } & Pick; export type DialogContextValue = { - triggerRef: React.Ref; - contentRef: React.Ref; + /** + * Reference to trigger element that opened the Dialog + * null if Dialog is closed + */ + triggerRef: React.RefObject; + contentRef: React.RefObject; modalType: DialogModalType; open: boolean; /** From 27e1544c31a1918963b5cc604baeb45754a193c7 Mon Sep 17 00:00:00 2001 From: Bernardo Sunderhus Date: Wed, 13 Jul 2022 11:12:05 +0000 Subject: [PATCH 11/17] Adds DialogBody component --- .../react-dialog/etc/react-dialog.api.md | 32 +++++++++++++-- .../react-dialog/src/DialogBody.ts | 1 + .../components/DialogBody/DialogBody.test.tsx | 18 +++++++++ .../src/components/DialogBody/DialogBody.tsx | 18 +++++++++ .../components/DialogBody/DialogBody.types.ts | 17 ++++++++ .../src/components/DialogBody/index.ts | 5 +++ .../DialogBody/renderDialogBody.tsx | 13 +++++++ .../components/DialogBody/useDialogBody.ts | 28 +++++++++++++ .../DialogBody/useDialogBodyStyles.ts | 39 +++++++++++++++++++ .../DialogContent/useDialogContentStyles.ts | 6 +-- .../DialogTitle/useDialogTitleStyles.ts | 5 ++- .../react-dialog/src/contexts/constants.ts | 1 + .../react-dialog/src/index.ts | 9 +++++ .../stories/Dialog/DialogDefault.stories.tsx | 4 +- 14 files changed, 186 insertions(+), 10 deletions(-) create mode 100644 packages/react-components/react-dialog/src/DialogBody.ts create mode 100644 packages/react-components/react-dialog/src/components/DialogBody/DialogBody.test.tsx create mode 100644 packages/react-components/react-dialog/src/components/DialogBody/DialogBody.tsx create mode 100644 packages/react-components/react-dialog/src/components/DialogBody/DialogBody.types.ts create mode 100644 packages/react-components/react-dialog/src/components/DialogBody/index.ts create mode 100644 packages/react-components/react-dialog/src/components/DialogBody/renderDialogBody.tsx create mode 100644 packages/react-components/react-dialog/src/components/DialogBody/useDialogBody.ts create mode 100644 packages/react-components/react-dialog/src/components/DialogBody/useDialogBodyStyles.ts 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 cbcf89a6bc462..fcbd28b05fbbb 100644 --- a/packages/react-components/react-dialog/etc/react-dialog.api.md +++ b/packages/react-components/react-dialog/etc/react-dialog.api.md @@ -20,6 +20,23 @@ import type { SlotClassNames } from '@fluentui/react-utilities'; // @public (undocumented) export const Dialog: ForwardRefComponent; +// @public +export const DialogBody: ForwardRefComponent; + +// @public (undocumented) +export const dialogBodyClassNames: SlotClassNames; + +// @public +export type DialogBodyProps = ComponentProps & {}; + +// @public (undocumented) +export type DialogBodySlots = { + root: Slot<'div'>; +}; + +// @public +export type DialogBodyState = ComponentState; + // @public (undocumented) export const dialogClassNames: SlotClassNames; @@ -45,14 +62,14 @@ export type DialogProps = ComponentProps> & { modalType?: DialogModalType; open?: boolean; defaultOpen?: boolean; - onOpenChange?(...args: DialogOpenChangeArgs): void; + onOpenChange?: DialogOnOpenChange; children: [JSX.Element, JSX.Element] | JSX.Element; }; // @public (undocumented) export type DialogSlots = { - overlay: Slot<'div'>; - root: Slot<'div'>; + overlay?: Slot<'div'>; + root: NonNullable>; }; // @public (undocumented) @@ -106,6 +123,9 @@ export type DialogTriggerState = { // @public export const renderDialog_unstable: (state: DialogState, contextValues: DialogContextValues) => JSX.Element; +// @public +export const renderDialogBody_unstable: (state: DialogBodyState) => JSX.Element; + // @public export const renderDialogContent_unstable: (state: DialogContentState) => JSX.Element; @@ -118,6 +138,12 @@ export const renderDialogTrigger_unstable: (state: DialogTriggerState) => ReactE // @public export const useDialog_unstable: (props: DialogProps, ref: React_2.Ref) => DialogState; +// @public +export const useDialogBody_unstable: (props: DialogBodyProps, ref: React_2.Ref) => DialogBodyState; + +// @public +export const useDialogBodyStyles_unstable: (state: DialogBodyState) => DialogBodyState; + // @public export const useDialogContent_unstable: (props: DialogContentProps, ref: React_2.Ref) => DialogContentState; diff --git a/packages/react-components/react-dialog/src/DialogBody.ts b/packages/react-components/react-dialog/src/DialogBody.ts new file mode 100644 index 0000000000000..9256da141252b --- /dev/null +++ b/packages/react-components/react-dialog/src/DialogBody.ts @@ -0,0 +1 @@ +export * from './components/DialogBody/index'; diff --git a/packages/react-components/react-dialog/src/components/DialogBody/DialogBody.test.tsx b/packages/react-components/react-dialog/src/components/DialogBody/DialogBody.test.tsx new file mode 100644 index 0000000000000..40ac2009d38ea --- /dev/null +++ b/packages/react-components/react-dialog/src/components/DialogBody/DialogBody.test.tsx @@ -0,0 +1,18 @@ +import * as React from 'react'; +import { render } from '@testing-library/react'; +import { DialogBody } from './DialogBody'; +import { isConformant } from '../../common/isConformant'; + +describe('DialogBody', () => { + isConformant({ + Component: DialogBody, + displayName: 'DialogBody', + }); + + // TODO add more tests here, and create visual regression tests in /apps/vr-tests + + it('renders a default state', () => { + const result = render(Default DialogBody); + expect(result.container).toMatchSnapshot(); + }); +}); diff --git a/packages/react-components/react-dialog/src/components/DialogBody/DialogBody.tsx b/packages/react-components/react-dialog/src/components/DialogBody/DialogBody.tsx new file mode 100644 index 0000000000000..42f8d8c96bd9c --- /dev/null +++ b/packages/react-components/react-dialog/src/components/DialogBody/DialogBody.tsx @@ -0,0 +1,18 @@ +import * as React from 'react'; +import { useDialogBody_unstable } from './useDialogBody'; +import { renderDialogBody_unstable } from './renderDialogBody'; +import { useDialogBodyStyles_unstable } from './useDialogBodyStyles'; +import type { DialogBodyProps } from './DialogBody.types'; +import type { ForwardRefComponent } from '@fluentui/react-utilities'; + +/** + * DialogBody component - TODO: add more docs + */ +export const DialogBody: ForwardRefComponent = React.forwardRef((props, ref) => { + const state = useDialogBody_unstable(props, ref); + + useDialogBodyStyles_unstable(state); + return renderDialogBody_unstable(state); +}); + +DialogBody.displayName = 'DialogBody'; diff --git a/packages/react-components/react-dialog/src/components/DialogBody/DialogBody.types.ts b/packages/react-components/react-dialog/src/components/DialogBody/DialogBody.types.ts new file mode 100644 index 0000000000000..6cfdec68aa8d7 --- /dev/null +++ b/packages/react-components/react-dialog/src/components/DialogBody/DialogBody.types.ts @@ -0,0 +1,17 @@ +import type { ComponentProps, ComponentState, Slot } from '@fluentui/react-utilities'; + +export type DialogBodySlots = { + root: Slot<'div'>; +}; + +/** + * DialogBody Props + */ +export type DialogBodyProps = ComponentProps & {}; + +/** + * State used in rendering DialogBody + */ +export type DialogBodyState = ComponentState; +// TODO: Remove semicolon from previous line, uncomment next line, and provide union of props to pick from DialogBodyProps. +// & Required> diff --git a/packages/react-components/react-dialog/src/components/DialogBody/index.ts b/packages/react-components/react-dialog/src/components/DialogBody/index.ts new file mode 100644 index 0000000000000..caade527cc447 --- /dev/null +++ b/packages/react-components/react-dialog/src/components/DialogBody/index.ts @@ -0,0 +1,5 @@ +export * from './DialogBody'; +export * from './DialogBody.types'; +export * from './renderDialogBody'; +export * from './useDialogBody'; +export * from './useDialogBodyStyles'; diff --git a/packages/react-components/react-dialog/src/components/DialogBody/renderDialogBody.tsx b/packages/react-components/react-dialog/src/components/DialogBody/renderDialogBody.tsx new file mode 100644 index 0000000000000..22d43bee81226 --- /dev/null +++ b/packages/react-components/react-dialog/src/components/DialogBody/renderDialogBody.tsx @@ -0,0 +1,13 @@ +import * as React from 'react'; +import { getSlots } from '@fluentui/react-utilities'; +import type { DialogBodyState, DialogBodySlots } from './DialogBody.types'; + +/** + * Render the final JSX of DialogBody + */ +export const renderDialogBody_unstable = (state: DialogBodyState) => { + const { slots, slotProps } = getSlots(state); + + // TODO Add additional slots in the appropriate place + return ; +}; diff --git a/packages/react-components/react-dialog/src/components/DialogBody/useDialogBody.ts b/packages/react-components/react-dialog/src/components/DialogBody/useDialogBody.ts new file mode 100644 index 0000000000000..e902284abc4db --- /dev/null +++ b/packages/react-components/react-dialog/src/components/DialogBody/useDialogBody.ts @@ -0,0 +1,28 @@ +import * as React from 'react'; +import { getNativeElementProps } from '@fluentui/react-utilities'; +import type { DialogBodyProps, DialogBodyState } from './DialogBody.types'; + +/** + * Create the state required to render DialogBody. + * + * The returned state can be modified with hooks such as useDialogBodyStyles_unstable, + * before being passed to renderDialogBody_unstable. + * + * @param props - props from this instance of DialogBody + * @param ref - reference to root HTMLElement of DialogBody + */ +export const useDialogBody_unstable = (props: DialogBodyProps, ref: React.Ref): DialogBodyState => { + return { + // TODO add appropriate props/defaults + components: { + // TODO add each slot's element type or component + root: 'div', + }, + // TODO add appropriate slots, for example: + // mySlot: resolveShorthand(props.mySlot), + root: getNativeElementProps('div', { + ref, + ...props, + }), + }; +}; diff --git a/packages/react-components/react-dialog/src/components/DialogBody/useDialogBodyStyles.ts b/packages/react-components/react-dialog/src/components/DialogBody/useDialogBodyStyles.ts new file mode 100644 index 0000000000000..dee3b752b5df1 --- /dev/null +++ b/packages/react-components/react-dialog/src/components/DialogBody/useDialogBodyStyles.ts @@ -0,0 +1,39 @@ +import { makeStyles, mergeClasses, shorthands } from '@griffel/react'; +import type { DialogBodySlots, DialogBodyState } from './DialogBody.types'; +import type { SlotClassNames } from '@fluentui/react-utilities'; +import { typographyStyles } from '@fluentui/react-theme'; +import { DIALOG_CONTENT_PADDING } from '../../contexts/constants'; + +export const dialogBodyClassName = 'fui-DialogBody'; +export const dialogBodyClassNames: SlotClassNames = { + root: 'fui-DialogBody', +}; + +/** + * Styles for the root slot + */ +const useStyles = makeStyles({ + root: { + display: 'flex', + flexDirection: 'row', + alignItems: 'start', + width: '100%', + height: 'fit-content', + minHeight: '32px', + ...shorthands.padding('0', DIALOG_CONTENT_PADDING), + ...typographyStyles.body1, + }, +}); + +/** + * Apply styling to the DialogBody slots based on the state + */ +export const useDialogBodyStyles_unstable = (state: DialogBodyState): DialogBodyState => { + const styles = useStyles(); + state.root.className = mergeClasses(dialogBodyClassName, styles.root, state.root.className); + + // TODO Add class names to slots, for example: + // state.mySlot.className = mergeClasses(styles.mySlot, state.mySlot.className); + + return state; +}; diff --git a/packages/react-components/react-dialog/src/components/DialogContent/useDialogContentStyles.ts b/packages/react-components/react-dialog/src/components/DialogContent/useDialogContentStyles.ts index 839a02c9923c3..5699d6352f4ee 100644 --- a/packages/react-components/react-dialog/src/components/DialogContent/useDialogContentStyles.ts +++ b/packages/react-components/react-dialog/src/components/DialogContent/useDialogContentStyles.ts @@ -19,18 +19,18 @@ const useStyles = makeStyles({ transform: 'translate(-50%, -50%)', display: 'flex', flexDirection: 'column', - width: 'fit-content', + width: '100%', height: 'fit-content', maxWidth: '600px', maxHeight: '100vh', boxShadow: tokens.shadow64, backgroundColor: tokens.colorNeutralBackground1, + ...shorthands.borderRadius('8px'), + ...shorthands.margin('auto'), [MEDIA_QUERY_BREAKPOINT_SELECTOR]: { maxWidth: '100vw', width: '100%', }, - ...shorthands.borderRadius('8px'), - ...shorthands.margin('auto'), }, }); diff --git a/packages/react-components/react-dialog/src/components/DialogTitle/useDialogTitleStyles.ts b/packages/react-components/react-dialog/src/components/DialogTitle/useDialogTitleStyles.ts index 6f71e7a5e4d6e..d80561735ab8c 100644 --- a/packages/react-components/react-dialog/src/components/DialogTitle/useDialogTitleStyles.ts +++ b/packages/react-components/react-dialog/src/components/DialogTitle/useDialogTitleStyles.ts @@ -3,6 +3,7 @@ import type { DialogTitleSlots, DialogTitleState } from './DialogTitle.types'; import type { SlotClassNames } from '@fluentui/react-utilities'; import { createFocusOutlineStyle } from '@fluentui/react-tabster'; import { typographyStyles } from '@fluentui/react-theme'; +import { DIALOG_CONTENT_PADDING } from '../../contexts/constants'; export const dialogTitleClassNames: SlotClassNames = { root: 'fui-DialogTitle', @@ -21,10 +22,10 @@ const useStyles = makeStyles({ ...typographyStyles.subtitle1, }, rootWithoutCloseButton: { - ...shorthands.padding('24px', '24px', '8px', '24px'), + ...shorthands.padding(DIALOG_CONTENT_PADDING, DIALOG_CONTENT_PADDING, '8px', DIALOG_CONTENT_PADDING), }, rootWithCloseButton: { - ...shorthands.padding('24px', '20px', '8px', '24px'), + ...shorthands.padding(DIALOG_CONTENT_PADDING, '20px', '8px', DIALOG_CONTENT_PADDING), }, closeButton: { position: 'relative', diff --git a/packages/react-components/react-dialog/src/contexts/constants.ts b/packages/react-components/react-dialog/src/contexts/constants.ts index 37af304dcb2d3..ddde25ad12518 100644 --- a/packages/react-components/react-dialog/src/contexts/constants.ts +++ b/packages/react-components/react-dialog/src/contexts/constants.ts @@ -1 +1,2 @@ export const MEDIA_QUERY_BREAKPOINT_SELECTOR = '@media screen and (max-width: 480px)'; +export const DIALOG_CONTENT_PADDING = '24px'; diff --git a/packages/react-components/react-dialog/src/index.ts b/packages/react-components/react-dialog/src/index.ts index 2ad28657ba097..091abb469ed32 100644 --- a/packages/react-components/react-dialog/src/index.ts +++ b/packages/react-components/react-dialog/src/index.ts @@ -33,3 +33,12 @@ export { renderDialogTitle_unstable, } from './DialogTitle'; export type { DialogTitleProps, DialogTitleSlots, DialogTitleState } from './DialogTitle'; + +export { + DialogBody, + dialogBodyClassNames, + useDialogBodyStyles_unstable, + useDialogBody_unstable, + renderDialogBody_unstable, +} from './DialogBody'; +export type { DialogBodyProps, DialogBodySlots, DialogBodyState } from './DialogBody'; diff --git a/packages/react-components/react-dialog/src/stories/Dialog/DialogDefault.stories.tsx b/packages/react-components/react-dialog/src/stories/Dialog/DialogDefault.stories.tsx index 70bfc8269c63d..486b51f37cbf2 100644 --- a/packages/react-components/react-dialog/src/stories/Dialog/DialogDefault.stories.tsx +++ b/packages/react-components/react-dialog/src/stories/Dialog/DialogDefault.stories.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { Dialog, DialogTrigger, DialogContent, DialogTitle } from '@fluentui/react-dialog'; +import { Dialog, DialogTrigger, DialogContent, DialogTitle, DialogBody } from '@fluentui/react-dialog'; import type { DialogProps } from '@fluentui/react-dialog'; import { Button } from '@fluentui/react-components'; @@ -12,7 +12,7 @@ export const Default = (props: Partial) => { Dialog Title - Dialog Content + Dialog Content From 46d53cc8cd9b9a791f4ccc6ef667ed519a38c4e9 Mon Sep 17 00:00:00 2001 From: Bernardo Sunderhus Date: Wed, 13 Jul 2022 11:13:25 +0000 Subject: [PATCH 12/17] Adds DialogActions component --- .../react-dialog/etc/react-dialog.api.md | 29 ++++++++++++++++ .../react-dialog/src/DialogActions.ts | 1 + .../DialogActions/DialogActions.test.tsx | 18 ++++++++++ .../DialogActions/DialogActions.tsx | 18 ++++++++++ .../DialogActions/DialogActions.types.ts | 17 ++++++++++ .../src/components/DialogActions/index.ts | 5 +++ .../DialogActions/renderDialogActions.tsx | 13 +++++++ .../DialogActions/useDialogActions.ts | 31 +++++++++++++++++ .../DialogActions/useDialogActionsStyles.ts | 34 +++++++++++++++++++ .../react-dialog/src/index.ts | 9 +++++ 10 files changed, 175 insertions(+) create mode 100644 packages/react-components/react-dialog/src/DialogActions.ts create mode 100644 packages/react-components/react-dialog/src/components/DialogActions/DialogActions.test.tsx create mode 100644 packages/react-components/react-dialog/src/components/DialogActions/DialogActions.tsx create mode 100644 packages/react-components/react-dialog/src/components/DialogActions/DialogActions.types.ts create mode 100644 packages/react-components/react-dialog/src/components/DialogActions/index.ts create mode 100644 packages/react-components/react-dialog/src/components/DialogActions/renderDialogActions.tsx create mode 100644 packages/react-components/react-dialog/src/components/DialogActions/useDialogActions.ts create mode 100644 packages/react-components/react-dialog/src/components/DialogActions/useDialogActionsStyles.ts 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 fcbd28b05fbbb..55b5233175626 100644 --- a/packages/react-components/react-dialog/etc/react-dialog.api.md +++ b/packages/react-components/react-dialog/etc/react-dialog.api.md @@ -20,6 +20,26 @@ import type { SlotClassNames } from '@fluentui/react-utilities'; // @public (undocumented) export const Dialog: ForwardRefComponent; +// @public +export const DialogActions: ForwardRefComponent; + +// @public (undocumented) +export const dialogActionsClassName = "fui-DialogActions"; + +// @public (undocumented) +export const dialogActionsClassNames: SlotClassNames; + +// @public +export type DialogActionsProps = ComponentProps & {}; + +// @public (undocumented) +export type DialogActionsSlots = { + root: Slot<'div'>; +}; + +// @public +export type DialogActionsState = ComponentState; + // @public export const DialogBody: ForwardRefComponent; @@ -123,6 +143,9 @@ export type DialogTriggerState = { // @public export const renderDialog_unstable: (state: DialogState, contextValues: DialogContextValues) => JSX.Element; +// @public +export const renderDialogActions_unstable: (state: DialogActionsState) => JSX.Element; + // @public export const renderDialogBody_unstable: (state: DialogBodyState) => JSX.Element; @@ -138,6 +161,12 @@ export const renderDialogTrigger_unstable: (state: DialogTriggerState) => ReactE // @public export const useDialog_unstable: (props: DialogProps, ref: React_2.Ref) => DialogState; +// @public +export const useDialogActions_unstable: (props: DialogActionsProps, ref: React_2.Ref) => DialogActionsState; + +// @public +export const useDialogActionsStyles_unstable: (state: DialogActionsState) => DialogActionsState; + // @public export const useDialogBody_unstable: (props: DialogBodyProps, ref: React_2.Ref) => DialogBodyState; diff --git a/packages/react-components/react-dialog/src/DialogActions.ts b/packages/react-components/react-dialog/src/DialogActions.ts new file mode 100644 index 0000000000000..1d8ac02365d2d --- /dev/null +++ b/packages/react-components/react-dialog/src/DialogActions.ts @@ -0,0 +1 @@ +export * from './components/DialogActions/index'; diff --git a/packages/react-components/react-dialog/src/components/DialogActions/DialogActions.test.tsx b/packages/react-components/react-dialog/src/components/DialogActions/DialogActions.test.tsx new file mode 100644 index 0000000000000..38a29a26516da --- /dev/null +++ b/packages/react-components/react-dialog/src/components/DialogActions/DialogActions.test.tsx @@ -0,0 +1,18 @@ +import * as React from 'react'; +import { render } from '@testing-library/react'; +import { DialogActions } from './DialogActions'; +import { isConformant } from '../../common/isConformant'; + +describe('DialogActions', () => { + isConformant({ + Component: DialogActions, + displayName: 'DialogActions', + }); + + // TODO add more tests here, and create visual regression tests in /apps/vr-tests + + it('renders a default state', () => { + const result = render(Default DialogActions); + expect(result.container).toMatchSnapshot(); + }); +}); diff --git a/packages/react-components/react-dialog/src/components/DialogActions/DialogActions.tsx b/packages/react-components/react-dialog/src/components/DialogActions/DialogActions.tsx new file mode 100644 index 0000000000000..662152550980e --- /dev/null +++ b/packages/react-components/react-dialog/src/components/DialogActions/DialogActions.tsx @@ -0,0 +1,18 @@ +import * as React from 'react'; +import { useDialogActions_unstable } from './useDialogActions'; +import { renderDialogActions_unstable } from './renderDialogActions'; +import { useDialogActionsStyles_unstable } from './useDialogActionsStyles'; +import type { DialogActionsProps } from './DialogActions.types'; +import type { ForwardRefComponent } from '@fluentui/react-utilities'; + +/** + * DialogActions component - TODO: add more docs + */ +export const DialogActions: ForwardRefComponent = React.forwardRef((props, ref) => { + const state = useDialogActions_unstable(props, ref); + + useDialogActionsStyles_unstable(state); + return renderDialogActions_unstable(state); +}); + +DialogActions.displayName = 'DialogActions'; diff --git a/packages/react-components/react-dialog/src/components/DialogActions/DialogActions.types.ts b/packages/react-components/react-dialog/src/components/DialogActions/DialogActions.types.ts new file mode 100644 index 0000000000000..41184f91097cc --- /dev/null +++ b/packages/react-components/react-dialog/src/components/DialogActions/DialogActions.types.ts @@ -0,0 +1,17 @@ +import type { ComponentProps, ComponentState, Slot } from '@fluentui/react-utilities'; + +export type DialogActionsSlots = { + root: Slot<'div'>; +}; + +/** + * DialogActions Props + */ +export type DialogActionsProps = ComponentProps & {}; + +/** + * State used in rendering DialogActions + */ +export type DialogActionsState = ComponentState; +// TODO: Remove semicolon from previous line, uncomment next line, and provide union of props to pick from DialogActionsProps. +// & Required> diff --git a/packages/react-components/react-dialog/src/components/DialogActions/index.ts b/packages/react-components/react-dialog/src/components/DialogActions/index.ts new file mode 100644 index 0000000000000..5b6fc48233a89 --- /dev/null +++ b/packages/react-components/react-dialog/src/components/DialogActions/index.ts @@ -0,0 +1,5 @@ +export * from './DialogActions'; +export * from './DialogActions.types'; +export * from './renderDialogActions'; +export * from './useDialogActions'; +export * from './useDialogActionsStyles'; diff --git a/packages/react-components/react-dialog/src/components/DialogActions/renderDialogActions.tsx b/packages/react-components/react-dialog/src/components/DialogActions/renderDialogActions.tsx new file mode 100644 index 0000000000000..edc1d553f7820 --- /dev/null +++ b/packages/react-components/react-dialog/src/components/DialogActions/renderDialogActions.tsx @@ -0,0 +1,13 @@ +import * as React from 'react'; +import { getSlots } from '@fluentui/react-utilities'; +import type { DialogActionsState, DialogActionsSlots } from './DialogActions.types'; + +/** + * Render the final JSX of DialogActions + */ +export const renderDialogActions_unstable = (state: DialogActionsState) => { + const { slots, slotProps } = getSlots(state); + + // TODO Add additional slots in the appropriate place + return ; +}; diff --git a/packages/react-components/react-dialog/src/components/DialogActions/useDialogActions.ts b/packages/react-components/react-dialog/src/components/DialogActions/useDialogActions.ts new file mode 100644 index 0000000000000..04a8ec6878ca8 --- /dev/null +++ b/packages/react-components/react-dialog/src/components/DialogActions/useDialogActions.ts @@ -0,0 +1,31 @@ +import * as React from 'react'; +import { getNativeElementProps } from '@fluentui/react-utilities'; +import type { DialogActionsProps, DialogActionsState } from './DialogActions.types'; + +/** + * Create the state required to render DialogActions. + * + * The returned state can be modified with hooks such as useDialogActionsStyles_unstable, + * before being passed to renderDialogActions_unstable. + * + * @param props - props from this instance of DialogActions + * @param ref - reference to root HTMLElement of DialogActions + */ +export const useDialogActions_unstable = ( + props: DialogActionsProps, + ref: React.Ref, +): DialogActionsState => { + return { + // TODO add appropriate props/defaults + components: { + // TODO add each slot's element type or component + root: 'div', + }, + // TODO add appropriate slots, for example: + // mySlot: resolveShorthand(props.mySlot), + root: getNativeElementProps('div', { + ref, + ...props, + }), + }; +}; diff --git a/packages/react-components/react-dialog/src/components/DialogActions/useDialogActionsStyles.ts b/packages/react-components/react-dialog/src/components/DialogActions/useDialogActionsStyles.ts new file mode 100644 index 0000000000000..ec14e2b127ac4 --- /dev/null +++ b/packages/react-components/react-dialog/src/components/DialogActions/useDialogActionsStyles.ts @@ -0,0 +1,34 @@ +import { makeStyles, mergeClasses } from '@griffel/react'; +import type { DialogActionsSlots, DialogActionsState } from './DialogActions.types'; +import type { SlotClassNames } from '@fluentui/react-utilities'; + +export const dialogActionsClassName = 'fui-DialogActions'; +export const dialogActionsClassNames: SlotClassNames = { + root: 'fui-DialogActions', + // TODO: add class names for all slots on DialogActionsSlots. + // Should be of the form `: 'fui-DialogActions__` +}; + +/** + * Styles for the root slot + */ +const useStyles = makeStyles({ + root: { + // TODO Add default styles for the root element + }, + + // TODO add additional classes for different states and/or slots +}); + +/** + * Apply styling to the DialogActions slots based on the state + */ +export const useDialogActionsStyles_unstable = (state: DialogActionsState): DialogActionsState => { + const styles = useStyles(); + state.root.className = mergeClasses(dialogActionsClassName, styles.root, state.root.className); + + // TODO Add class names to slots, for example: + // state.mySlot.className = mergeClasses(styles.mySlot, state.mySlot.className); + + return state; +}; diff --git a/packages/react-components/react-dialog/src/index.ts b/packages/react-components/react-dialog/src/index.ts index 091abb469ed32..88f615ccb9a93 100644 --- a/packages/react-components/react-dialog/src/index.ts +++ b/packages/react-components/react-dialog/src/index.ts @@ -42,3 +42,12 @@ export { renderDialogBody_unstable, } from './DialogBody'; export type { DialogBodyProps, DialogBodySlots, DialogBodyState } from './DialogBody'; + +export { + DialogActions, + dialogActionsClassNames, + useDialogActionsStyles_unstable, + useDialogActions_unstable, + renderDialogActions_unstable, +} from './DialogActions'; +export type { DialogActionsProps, DialogActionsSlots, DialogActionsState } from './DialogActions'; From a2757e13dda21f4829dcbbd80d5f1eea2042bc5e Mon Sep 17 00:00:00 2001 From: Bernardo Sunderhus Date: Wed, 13 Jul 2022 13:24:40 +0000 Subject: [PATCH 13/17] Removes root from Dialog --- .../src/components/Dialog/Dialog.tsx | 5 +-- .../src/components/Dialog/Dialog.types.ts | 1 - .../src/components/Dialog/renderDialog.tsx | 6 +-- .../src/components/Dialog/useDialog.ts | 38 ++----------------- .../src/components/Dialog/useDialogStyles.ts | 4 -- .../DialogContent/useDialogContent.ts | 17 +++++++-- .../react-dialog/src/utils/isEscapeKeyDown.ts | 12 ++++++ 7 files changed, 34 insertions(+), 49 deletions(-) create mode 100644 packages/react-components/react-dialog/src/utils/isEscapeKeyDown.ts diff --git a/packages/react-components/react-dialog/src/components/Dialog/Dialog.tsx b/packages/react-components/react-dialog/src/components/Dialog/Dialog.tsx index 131a351b5c8c9..3318843ba8ed3 100644 --- a/packages/react-components/react-dialog/src/components/Dialog/Dialog.tsx +++ b/packages/react-components/react-dialog/src/components/Dialog/Dialog.tsx @@ -4,10 +4,9 @@ import { renderDialog_unstable } from './renderDialog'; import { useDialogStyles_unstable } from './useDialogStyles'; import type { DialogProps } from './Dialog.types'; import { useDialogContextValues_unstable } from './useDialogContextValues'; -import { ForwardRefComponent } from '@fluentui/react-utilities'; -export const Dialog: ForwardRefComponent = React.forwardRef((props, ref) => { - const state = useDialog_unstable(props, ref); +export const Dialog: React.FC = React.memo(props => { + const state = useDialog_unstable(props); const contextValues = useDialogContextValues_unstable(state); useDialogStyles_unstable(state); 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 d590602e9fa3d..d2ed95176e4ee 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 @@ -10,7 +10,6 @@ export type DialogSlots = { * The overlay should have `aria-hidden="true"`. */ overlay?: Slot<'div'>; - root: NonNullable>; }; export type DialogOpenChangeArgs = diff --git a/packages/react-components/react-dialog/src/components/Dialog/renderDialog.tsx b/packages/react-components/react-dialog/src/components/Dialog/renderDialog.tsx index e4f6736f42314..632dadaacd8c8 100644 --- a/packages/react-components/react-dialog/src/components/Dialog/renderDialog.tsx +++ b/packages/react-components/react-dialog/src/components/Dialog/renderDialog.tsx @@ -16,10 +16,8 @@ export const renderDialog_unstable = (state: DialogState, contextValues: DialogC {trigger} {open && ( - - {slots.overlay && } - {content} - + {slots.overlay && } + {content} )} 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 a43f946c9b638..e549f6268077c 100644 --- a/packages/react-components/react-dialog/src/components/Dialog/useDialog.ts +++ b/packages/react-components/react-dialog/src/components/Dialog/useDialog.ts @@ -1,10 +1,5 @@ import * as React from 'react'; -import { - getNativeElementProps, - resolveShorthand, - useControllableState, - useEventCallback, -} from '@fluentui/react-utilities'; +import { resolveShorthand, useControllableState, useEventCallback } from '@fluentui/react-utilities'; import type { DialogOpenChangeArgs, DialogProps, DialogState, DialogModalType } from './Dialog.types'; import { DialogRequestOpenChangeData } from '../../contexts/dialogContext'; import { Escape } from '@fluentui/keyboard-keys'; @@ -12,6 +7,7 @@ import { useFocusFinders } from '@fluentui/react-tabster'; import { useFluent_unstable } from '@fluentui/react-shared-contexts'; import { normalizeDefaultPrevented } from '../../utils/normalizeDefaultPrevented'; import { normalizeSetStateAction } from '../../utils/normalizeSetStateAction'; +import { isEscapeKeyDismiss } from '../../utils/isEscapeKeyDown'; /** * Create the state required to render Dialog. @@ -21,8 +17,8 @@ import { normalizeSetStateAction } from '../../utils/normalizeSetStateAction'; * * @param props - props from this instance of Dialog */ -export const useDialog_unstable = (props: DialogProps, ref: React.Ref): DialogState => { - const { children, overlay, modalType = 'modal', onOpenChange, as = 'div' } = props; +export const useDialog_unstable = (props: DialogProps): DialogState => { + const { children, overlay, modalType = 'modal', onOpenChange } = props; const [trigger, content] = childrenToTriggerAndContent(children); @@ -32,11 +28,6 @@ export const useDialog_unstable = (props: DialogProps, ref: React.Ref { - rootShorthand.onKeyDown?.(event); - if (isEscapeKeyDismiss(event, modalType)) { - requestOpenChange({ event, open: false, type: 'escapeKeyDown' }); - event.preventDefault(); - } - }); - return { components: { overlay: 'div', - root: 'div', }, overlay: overlayShorthand && { ...overlayShorthand, onClick: handleOverLayClick, }, - root: { - ...rootShorthand, - onKeyDown: handleRootKeyDown, - }, open, modalType, content, @@ -142,14 +120,6 @@ function childrenToTriggerAndContent( } } -/** - * Checks if keydown event is a proper Escape key dismiss - */ -function isEscapeKeyDismiss(event: React.KeyboardEvent | KeyboardEvent, type: DialogModalType): boolean { - const isDefaultPrevented = normalizeDefaultPrevented(event); - return event.key === Escape && type !== 'alert' && !isDefaultPrevented(); -} - /** * Checks is click event is a proper Overlay click dismiss */ diff --git a/packages/react-components/react-dialog/src/components/Dialog/useDialogStyles.ts b/packages/react-components/react-dialog/src/components/Dialog/useDialogStyles.ts index 70d211cb37e99..ca37111b43061 100644 --- a/packages/react-components/react-dialog/src/components/Dialog/useDialogStyles.ts +++ b/packages/react-components/react-dialog/src/components/Dialog/useDialogStyles.ts @@ -3,14 +3,12 @@ import { makeStyles, mergeClasses, shorthands } from '@griffel/react'; import type { DialogSlots, DialogState } from './Dialog.types'; export const dialogClassNames: SlotClassNames = { - root: 'fui-Dialog', overlay: 'fui-Dialog__overlay', }; /** * Styles for the root slot */ const useStyles = makeStyles({ - root: {}, overlay: { position: 'fixed', backgroundColor: 'rgba(0, 0, 0, 0.4)', @@ -24,8 +22,6 @@ const useStyles = makeStyles({ export const useDialogStyles_unstable = (state: DialogState): DialogState => { const styles = useStyles(); - state.root.className = mergeClasses(dialogClassNames.root, styles.root, state.root.className); - if (state.overlay) { state.overlay.className = mergeClasses(dialogClassNames.overlay, styles.overlay, state.overlay.className); } diff --git a/packages/react-components/react-dialog/src/components/DialogContent/useDialogContent.ts b/packages/react-components/react-dialog/src/components/DialogContent/useDialogContent.ts index 138abbc6943a1..98fbac9de263f 100644 --- a/packages/react-components/react-dialog/src/components/DialogContent/useDialogContent.ts +++ b/packages/react-components/react-dialog/src/components/DialogContent/useDialogContent.ts @@ -1,8 +1,9 @@ import * as React from 'react'; -import { getNativeElementProps, useMergedRefs } from '@fluentui/react-utilities'; +import { getNativeElementProps, useEventCallback, useMergedRefs } from '@fluentui/react-utilities'; import type { DialogContentProps, DialogContentState } from './DialogContent.types'; import { useDialogContext_unstable } from '../../contexts/dialogContext'; import { useModalAttributes } from '@fluentui/react-tabster'; +import { isEscapeKeyDismiss } from '../../utils/isEscapeKeyDown'; /** * Create the state required to render DialogContent. @@ -20,10 +21,19 @@ export const useDialogContent_unstable = ( const { as = 'div' } = props; const contentRef = useDialogContext_unstable(ctx => ctx.contentRef); - const type = useDialogContext_unstable(ctx => ctx.modalType); + const requestOpenChange = useDialogContext_unstable(ctx => ctx.requestOpenChange); + const modalType = useDialogContext_unstable(ctx => ctx.modalType); const { modalAttributes } = useModalAttributes({ - trapFocus: type !== 'non-modal', + trapFocus: modalType !== 'non-modal', + }); + + const handleRootKeyDown = useEventCallback((event: React.KeyboardEvent) => { + props.onKeyDown?.(event); + if (isEscapeKeyDismiss(event, modalType)) { + requestOpenChange({ event, open: false, type: 'escapeKeyDown' }); + event.preventDefault(); + } }); return { @@ -34,6 +44,7 @@ export const useDialogContent_unstable = ( ref: useMergedRefs(ref, contentRef), ...props, ...modalAttributes, + onKeyDown: handleRootKeyDown, }), }; }; diff --git a/packages/react-components/react-dialog/src/utils/isEscapeKeyDown.ts b/packages/react-components/react-dialog/src/utils/isEscapeKeyDown.ts new file mode 100644 index 0000000000000..4d649898200b6 --- /dev/null +++ b/packages/react-components/react-dialog/src/utils/isEscapeKeyDown.ts @@ -0,0 +1,12 @@ +import * as React from 'react'; +import { Escape } from '@fluentui/keyboard-keys'; +import { DialogModalType } from '../components/Dialog/Dialog.types'; +import { normalizeDefaultPrevented } from './normalizeDefaultPrevented'; + +/** + * Checks if keydown event is a proper Escape key dismiss + */ +export function isEscapeKeyDismiss(event: React.KeyboardEvent | KeyboardEvent, type: DialogModalType): boolean { + const isDefaultPrevented = normalizeDefaultPrevented(event); + return event.key === Escape && type !== 'alert' && !isDefaultPrevented(); +} From 30587dd199c0a087ee3602c043423b504591a577 Mon Sep 17 00:00:00 2001 From: Bernardo Sunderhus Date: Wed, 13 Jul 2022 13:24:53 +0000 Subject: [PATCH 14/17] Adds styles for DialogActions --- .../DialogActions/useDialogActionsStyles.ts | 29 +++++++---- .../DialogBody/useDialogBodyStyles.ts | 5 +- .../Dialog/DialogChangeFocus.stories.tsx | 52 +++++++++++++++++++ .../stories/Dialog/DialogDefault.stories.tsx | 11 ++-- .../stories/Dialog/DialogNested.stories.tsx | 30 ++++++----- .../DialogNoFocusableElement.stories.tsx | 14 +++-- .../stories/Dialog/DialogNonModal.stories.tsx | 12 +++-- .../src/stories/Dialog/index.stories.tsx | 1 + 8 files changed, 114 insertions(+), 40 deletions(-) create mode 100644 packages/react-components/react-dialog/src/stories/Dialog/DialogChangeFocus.stories.tsx diff --git a/packages/react-components/react-dialog/src/components/DialogActions/useDialogActionsStyles.ts b/packages/react-components/react-dialog/src/components/DialogActions/useDialogActionsStyles.ts index ec14e2b127ac4..63d7444e55353 100644 --- a/packages/react-components/react-dialog/src/components/DialogActions/useDialogActionsStyles.ts +++ b/packages/react-components/react-dialog/src/components/DialogActions/useDialogActionsStyles.ts @@ -1,12 +1,11 @@ -import { makeStyles, mergeClasses } from '@griffel/react'; +import { makeStyles, mergeClasses, shorthands } from '@griffel/react'; import type { DialogActionsSlots, DialogActionsState } from './DialogActions.types'; import type { SlotClassNames } from '@fluentui/react-utilities'; +import { DIALOG_CONTENT_PADDING, MEDIA_QUERY_BREAKPOINT_SELECTOR } from '../../contexts/constants'; export const dialogActionsClassName = 'fui-DialogActions'; export const dialogActionsClassNames: SlotClassNames = { root: 'fui-DialogActions', - // TODO: add class names for all slots on DialogActionsSlots. - // Should be of the form `: 'fui-DialogActions__` }; /** @@ -14,10 +13,24 @@ export const dialogActionsClassNames: SlotClassNames = { */ const useStyles = makeStyles({ root: { - // TODO Add default styles for the root element + display: 'flex', + flexDirection: 'row', + alignItems: 'end', + justifyContent: 'end', + height: 'fit-content', + width: '100%', + boxSizing: 'border-box', + ...shorthands.gap('8px'), + ...shorthands.padding('8px', DIALOG_CONTENT_PADDING, DIALOG_CONTENT_PADDING, DIALOG_CONTENT_PADDING), + [MEDIA_QUERY_BREAKPOINT_SELECTOR]: { + flexDirection: 'column', + alignItems: 'stretch', + paddingTop: '12px', + '> .fui-Button': { + maxWidth: '100%', + }, + }, }, - - // TODO add additional classes for different states and/or slots }); /** @@ -26,9 +39,5 @@ const useStyles = makeStyles({ export const useDialogActionsStyles_unstable = (state: DialogActionsState): DialogActionsState => { const styles = useStyles(); state.root.className = mergeClasses(dialogActionsClassName, styles.root, state.root.className); - - // TODO Add class names to slots, for example: - // state.mySlot.className = mergeClasses(styles.mySlot, state.mySlot.className); - return state; }; diff --git a/packages/react-components/react-dialog/src/components/DialogBody/useDialogBodyStyles.ts b/packages/react-components/react-dialog/src/components/DialogBody/useDialogBodyStyles.ts index dee3b752b5df1..ab373b96ce50a 100644 --- a/packages/react-components/react-dialog/src/components/DialogBody/useDialogBodyStyles.ts +++ b/packages/react-components/react-dialog/src/components/DialogBody/useDialogBodyStyles.ts @@ -14,12 +14,13 @@ export const dialogBodyClassNames: SlotClassNames = { */ const useStyles = makeStyles({ root: { - display: 'flex', - flexDirection: 'row', + // display: 'flex', + // flexDirection: 'row', alignItems: 'start', width: '100%', height: 'fit-content', minHeight: '32px', + boxSizing: 'border-box', ...shorthands.padding('0', DIALOG_CONTENT_PADDING), ...typographyStyles.body1, }, diff --git a/packages/react-components/react-dialog/src/stories/Dialog/DialogChangeFocus.stories.tsx b/packages/react-components/react-dialog/src/stories/Dialog/DialogChangeFocus.stories.tsx new file mode 100644 index 0000000000000..dc0bfe3b25b24 --- /dev/null +++ b/packages/react-components/react-dialog/src/stories/Dialog/DialogChangeFocus.stories.tsx @@ -0,0 +1,52 @@ +import * as React from 'react'; +import { Dialog, DialogTrigger, DialogContent, DialogTitle, DialogBody, DialogActions } from '@fluentui/react-dialog'; +import type { DialogProps } from '@fluentui/react-dialog'; +import { Button } from '@fluentui/react-components'; + +import { makeStyles } from '@griffel/react'; +import { DialogOnOpenChange } from '../../Dialog'; + +const useStyles = makeStyles({ + thirdAction: { + marginRight: 'auto', + '@media screen and (max-width: 480px)': { + marginRight: 'unset', + }, + }, +}); + +export const ChangeFocus = (props: Partial) => { + const styles = useStyles(); + const buttonRef = React.useRef(null); + const [open, setOpen] = React.useState(false); + const handleOpenChange: DialogOnOpenChange = (_, data) => setOpen(data.open); + React.useEffect(() => { + if (open) { + buttonRef.current?.focus(); + } + }, [open]); + return ( + <> + + + + + + Dialog Title + Dialog Content + + + + + + + + + + + ); +}; diff --git a/packages/react-components/react-dialog/src/stories/Dialog/DialogDefault.stories.tsx b/packages/react-components/react-dialog/src/stories/Dialog/DialogDefault.stories.tsx index 486b51f37cbf2..133c0f63310f6 100644 --- a/packages/react-components/react-dialog/src/stories/Dialog/DialogDefault.stories.tsx +++ b/packages/react-components/react-dialog/src/stories/Dialog/DialogDefault.stories.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { Dialog, DialogTrigger, DialogContent, DialogTitle, DialogBody } from '@fluentui/react-dialog'; +import { Dialog, DialogTrigger, DialogContent, DialogTitle, DialogBody, DialogActions } from '@fluentui/react-dialog'; import type { DialogProps } from '@fluentui/react-dialog'; import { Button } from '@fluentui/react-components'; @@ -13,9 +13,12 @@ export const Default = (props: Partial) => { Dialog Title Dialog Content - - - + + + + + +
diff --git a/packages/react-components/react-dialog/src/stories/Dialog/DialogNested.stories.tsx b/packages/react-components/react-dialog/src/stories/Dialog/DialogNested.stories.tsx index a86cf0054b07e..fec5aca3cbace 100644 --- a/packages/react-components/react-dialog/src/stories/Dialog/DialogNested.stories.tsx +++ b/packages/react-components/react-dialog/src/stories/Dialog/DialogNested.stories.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { Dialog, DialogTrigger, DialogContent, DialogTitle } from '@fluentui/react-dialog'; +import { Dialog, DialogTrigger, DialogContent, DialogTitle, DialogActions } from '@fluentui/react-dialog'; import type { DialogProps } from '@fluentui/react-dialog'; import { Button } from '@fluentui/react-components'; @@ -12,19 +12,21 @@ export const Nested = (props: Partial) => { Dialog Title - Dialog Content - - - - - - Inner Dialog Title - Dialog Content - {/* - - */} - - + + + + + + + Inner Dialog Title + + + + + + + +
diff --git a/packages/react-components/react-dialog/src/stories/Dialog/DialogNoFocusableElement.stories.tsx b/packages/react-components/react-dialog/src/stories/Dialog/DialogNoFocusableElement.stories.tsx index d10ff3ab586c4..9da62284f72ca 100644 --- a/packages/react-components/react-dialog/src/stories/Dialog/DialogNoFocusableElement.stories.tsx +++ b/packages/react-components/react-dialog/src/stories/Dialog/DialogNoFocusableElement.stories.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { Dialog, DialogTrigger, DialogContent, DialogTitle } from '@fluentui/react-dialog'; +import { Dialog, DialogTrigger, DialogContent, DialogTitle, DialogBody } from '@fluentui/react-dialog'; import type { DialogProps } from '@fluentui/react-dialog'; import { Button } from '@fluentui/react-components'; @@ -12,8 +12,10 @@ export const NoFocusableElement = (props: Partial) => { Dialog Title -

⚠️A Dialog without focusable elements is not recommended!⚠️

-

Escape key and overlay click still works to ensure this modal can be closed

+ +

⚠️A Dialog without focusable elements is not recommended!⚠️

+

Escape key and overlay click still works to ensure this modal can be closed

+
@@ -22,8 +24,10 @@ export const NoFocusableElement = (props: Partial) => {
Dialog Title -

⚠️A Dialog without focusable elements is not recommended!⚠️

-

Escape key still works to ensure this modal can be closed

+ +

⚠️A Dialog without focusable elements is not recommended!⚠️

+

Escape key still works to ensure this modal can be closed

+
diff --git a/packages/react-components/react-dialog/src/stories/Dialog/DialogNonModal.stories.tsx b/packages/react-components/react-dialog/src/stories/Dialog/DialogNonModal.stories.tsx index b9438fa068e2e..ebe61747223ae 100644 --- a/packages/react-components/react-dialog/src/stories/Dialog/DialogNonModal.stories.tsx +++ b/packages/react-components/react-dialog/src/stories/Dialog/DialogNonModal.stories.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { Dialog, DialogTrigger, DialogContent, DialogTitle } from '@fluentui/react-dialog'; +import { Dialog, DialogTrigger, DialogContent, DialogTitle, DialogBody, DialogActions } from '@fluentui/react-dialog'; import type { DialogProps } from '@fluentui/react-dialog'; import { Button } from '@fluentui/react-components'; @@ -12,10 +12,12 @@ export const NonModal = (props: Partial) => { Dialog Title - Dialog Content - - - + Dialog Content + + + + + diff --git a/packages/react-components/react-dialog/src/stories/Dialog/index.stories.tsx b/packages/react-components/react-dialog/src/stories/Dialog/index.stories.tsx index b8976a4692fc4..4d279c0dc3c64 100644 --- a/packages/react-components/react-dialog/src/stories/Dialog/index.stories.tsx +++ b/packages/react-components/react-dialog/src/stories/Dialog/index.stories.tsx @@ -7,6 +7,7 @@ export { Default } from './DialogDefault.stories'; export { NonModal } from './DialogNonModal.stories'; export { Nested } from './DialogNested.stories'; export { NoFocusableElement } from './DialogNoFocusableElement.stories'; +export { ChangeFocus } from './DialogChangeFocus.stories'; export default { title: 'Components/Dialog', From 1b4fa0fdf1f8893fe7ee67616132eccde5fe7963 Mon Sep 17 00:00:00 2001 From: Bernardo Sunderhus Date: Wed, 13 Jul 2022 15:54:11 +0000 Subject: [PATCH 15/17] Move stories --- .../react-dialog/src/stories/{Dialog => }/DialogBestPractices.md | 0 .../src/stories/{Dialog => }/DialogChangeFocus.stories.tsx | 0 .../src/stories/{Dialog => }/DialogDefault.stories.tsx | 0 .../react-dialog/src/stories/{Dialog => }/DialogDescription.md | 0 .../src/stories/{Dialog => }/DialogNested.stories.tsx | 0 .../src/stories/{Dialog => }/DialogNoFocusableElement.stories.tsx | 0 .../src/stories/{Dialog => }/DialogNonModal.stories.tsx | 0 .../react-dialog/src/stories/{Dialog => }/index.stories.tsx | 0 8 files changed, 0 insertions(+), 0 deletions(-) rename packages/react-components/react-dialog/src/stories/{Dialog => }/DialogBestPractices.md (100%) rename packages/react-components/react-dialog/src/stories/{Dialog => }/DialogChangeFocus.stories.tsx (100%) rename packages/react-components/react-dialog/src/stories/{Dialog => }/DialogDefault.stories.tsx (100%) rename packages/react-components/react-dialog/src/stories/{Dialog => }/DialogDescription.md (100%) rename packages/react-components/react-dialog/src/stories/{Dialog => }/DialogNested.stories.tsx (100%) rename packages/react-components/react-dialog/src/stories/{Dialog => }/DialogNoFocusableElement.stories.tsx (100%) rename packages/react-components/react-dialog/src/stories/{Dialog => }/DialogNonModal.stories.tsx (100%) rename packages/react-components/react-dialog/src/stories/{Dialog => }/index.stories.tsx (100%) diff --git a/packages/react-components/react-dialog/src/stories/Dialog/DialogBestPractices.md b/packages/react-components/react-dialog/src/stories/DialogBestPractices.md similarity index 100% rename from packages/react-components/react-dialog/src/stories/Dialog/DialogBestPractices.md rename to packages/react-components/react-dialog/src/stories/DialogBestPractices.md diff --git a/packages/react-components/react-dialog/src/stories/Dialog/DialogChangeFocus.stories.tsx b/packages/react-components/react-dialog/src/stories/DialogChangeFocus.stories.tsx similarity index 100% rename from packages/react-components/react-dialog/src/stories/Dialog/DialogChangeFocus.stories.tsx rename to packages/react-components/react-dialog/src/stories/DialogChangeFocus.stories.tsx diff --git a/packages/react-components/react-dialog/src/stories/Dialog/DialogDefault.stories.tsx b/packages/react-components/react-dialog/src/stories/DialogDefault.stories.tsx similarity index 100% rename from packages/react-components/react-dialog/src/stories/Dialog/DialogDefault.stories.tsx rename to packages/react-components/react-dialog/src/stories/DialogDefault.stories.tsx diff --git a/packages/react-components/react-dialog/src/stories/Dialog/DialogDescription.md b/packages/react-components/react-dialog/src/stories/DialogDescription.md similarity index 100% rename from packages/react-components/react-dialog/src/stories/Dialog/DialogDescription.md rename to packages/react-components/react-dialog/src/stories/DialogDescription.md diff --git a/packages/react-components/react-dialog/src/stories/Dialog/DialogNested.stories.tsx b/packages/react-components/react-dialog/src/stories/DialogNested.stories.tsx similarity index 100% rename from packages/react-components/react-dialog/src/stories/Dialog/DialogNested.stories.tsx rename to packages/react-components/react-dialog/src/stories/DialogNested.stories.tsx diff --git a/packages/react-components/react-dialog/src/stories/Dialog/DialogNoFocusableElement.stories.tsx b/packages/react-components/react-dialog/src/stories/DialogNoFocusableElement.stories.tsx similarity index 100% rename from packages/react-components/react-dialog/src/stories/Dialog/DialogNoFocusableElement.stories.tsx rename to packages/react-components/react-dialog/src/stories/DialogNoFocusableElement.stories.tsx diff --git a/packages/react-components/react-dialog/src/stories/Dialog/DialogNonModal.stories.tsx b/packages/react-components/react-dialog/src/stories/DialogNonModal.stories.tsx similarity index 100% rename from packages/react-components/react-dialog/src/stories/Dialog/DialogNonModal.stories.tsx rename to packages/react-components/react-dialog/src/stories/DialogNonModal.stories.tsx diff --git a/packages/react-components/react-dialog/src/stories/Dialog/index.stories.tsx b/packages/react-components/react-dialog/src/stories/index.stories.tsx similarity index 100% rename from packages/react-components/react-dialog/src/stories/Dialog/index.stories.tsx rename to packages/react-components/react-dialog/src/stories/index.stories.tsx From 6c64bdf4493b198f45bf7f5e136e0c8b3ef3254d Mon Sep 17 00:00:00 2001 From: Bernardo Sunderhus Date: Wed, 13 Jul 2022 17:44:51 +0000 Subject: [PATCH 16/17] Updates stories and styles --- .../src/components/Dialog/Dialog.types.ts | 5 ++-- .../src/components/Dialog/useDialog.ts | 5 ++-- .../src/components/Dialog/useDialogStyles.ts | 10 ++++++- .../DialogActions/useDialogActionsStyles.ts | 3 +- .../DialogBody/useDialogBodyStyles.ts | 2 +- .../DialogTitle/useDialogTitleStyles.ts | 1 + .../react-dialog/src/index.ts | 2 +- .../src/stories/DialogAlert.stories.tsx | 29 +++++++++++++++++++ .../src/stories/DialogChangeFocus.stories.tsx | 21 +++++++++----- .../src/stories/DialogDefault.stories.tsx | 8 +++-- .../src/stories/DialogNested.stories.tsx | 12 ++++---- .../src/stories/DialogNonModal.stories.tsx | 17 +++++------ .../src/stories/index.stories.tsx | 1 + 13 files changed, 83 insertions(+), 33 deletions(-) create mode 100644 packages/react-components/react-dialog/src/stories/DialogAlert.stories.tsx 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 d2ed95176e4ee..19789da74e7a3 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 @@ -27,7 +27,7 @@ export type DialogContextValues = { dialog: DialogContextValue; }; -export type DialogOnOpenChange = (...args: DialogOpenChangeArgs) => void; +export type DialogOpenChangeListener = (...args: DialogOpenChangeArgs) => void; export type DialogProps = ComponentProps> & { /** @@ -63,7 +63,7 @@ export type DialogProps = ComponentProps> & { * Callback fired when the component changes value from open state. * @default undefined */ - onOpenChange?: DialogOnOpenChange; + onOpenChange?: DialogOpenChangeListener; /** * Can contain two children including {@link DialogTrigger} and {@link DialogContent}. * Alternatively can only contain {@link DialogContent} if using trigger outside dialog, or controlling state. @@ -75,4 +75,5 @@ export type DialogState = ComponentState & DialogContextValue & { content: React.ReactNode; trigger: React.ReactNode; + isSubDialog: boolean; }; 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 e549f6268077c..851ef5221c22a 100644 --- a/packages/react-components/react-dialog/src/components/Dialog/useDialog.ts +++ b/packages/react-components/react-dialog/src/components/Dialog/useDialog.ts @@ -1,13 +1,13 @@ import * as React from 'react'; import { resolveShorthand, useControllableState, useEventCallback } from '@fluentui/react-utilities'; import type { DialogOpenChangeArgs, DialogProps, DialogState, DialogModalType } from './Dialog.types'; -import { DialogRequestOpenChangeData } from '../../contexts/dialogContext'; -import { Escape } from '@fluentui/keyboard-keys'; +import { DialogContext, DialogRequestOpenChangeData } from '../../contexts/dialogContext'; import { useFocusFinders } from '@fluentui/react-tabster'; import { useFluent_unstable } from '@fluentui/react-shared-contexts'; import { normalizeDefaultPrevented } from '../../utils/normalizeDefaultPrevented'; import { normalizeSetStateAction } from '../../utils/normalizeSetStateAction'; import { isEscapeKeyDismiss } from '../../utils/isEscapeKeyDown'; +import { useHasParentContext } from '@fluentui/react-context-selector'; /** * Create the state required to render Dialog. @@ -87,6 +87,7 @@ export const useDialog_unstable = (props: DialogProps): DialogState => { trigger, triggerRef, contentRef, + isSubDialog: useHasParentContext(DialogContext), requestOpenChange, }; }; diff --git a/packages/react-components/react-dialog/src/components/Dialog/useDialogStyles.ts b/packages/react-components/react-dialog/src/components/Dialog/useDialogStyles.ts index ca37111b43061..6f22a5b5642c8 100644 --- a/packages/react-components/react-dialog/src/components/Dialog/useDialogStyles.ts +++ b/packages/react-components/react-dialog/src/components/Dialog/useDialogStyles.ts @@ -14,6 +14,9 @@ const useStyles = makeStyles({ backgroundColor: 'rgba(0, 0, 0, 0.4)', ...shorthands.inset('0px'), }, + subDialogOverlay: { + backgroundColor: 'transparent', + }, }); /** @@ -23,7 +26,12 @@ export const useDialogStyles_unstable = (state: DialogState): DialogState => { const styles = useStyles(); if (state.overlay) { - state.overlay.className = mergeClasses(dialogClassNames.overlay, styles.overlay, state.overlay.className); + state.overlay.className = mergeClasses( + dialogClassNames.overlay, + styles.overlay, + state.isSubDialog && styles.subDialogOverlay, + state.overlay.className, + ); } return state; diff --git a/packages/react-components/react-dialog/src/components/DialogActions/useDialogActionsStyles.ts b/packages/react-components/react-dialog/src/components/DialogActions/useDialogActionsStyles.ts index 63d7444e55353..ac7061dd586d8 100644 --- a/packages/react-components/react-dialog/src/components/DialogActions/useDialogActionsStyles.ts +++ b/packages/react-components/react-dialog/src/components/DialogActions/useDialogActionsStyles.ts @@ -21,11 +21,10 @@ const useStyles = makeStyles({ width: '100%', boxSizing: 'border-box', ...shorthands.gap('8px'), - ...shorthands.padding('8px', DIALOG_CONTENT_PADDING, DIALOG_CONTENT_PADDING, DIALOG_CONTENT_PADDING), + ...shorthands.padding('0', DIALOG_CONTENT_PADDING, DIALOG_CONTENT_PADDING, DIALOG_CONTENT_PADDING), [MEDIA_QUERY_BREAKPOINT_SELECTOR]: { flexDirection: 'column', alignItems: 'stretch', - paddingTop: '12px', '> .fui-Button': { maxWidth: '100%', }, diff --git a/packages/react-components/react-dialog/src/components/DialogBody/useDialogBodyStyles.ts b/packages/react-components/react-dialog/src/components/DialogBody/useDialogBodyStyles.ts index ab373b96ce50a..9f364cf85833d 100644 --- a/packages/react-components/react-dialog/src/components/DialogBody/useDialogBodyStyles.ts +++ b/packages/react-components/react-dialog/src/components/DialogBody/useDialogBodyStyles.ts @@ -21,7 +21,7 @@ const useStyles = makeStyles({ height: 'fit-content', minHeight: '32px', boxSizing: 'border-box', - ...shorthands.padding('0', DIALOG_CONTENT_PADDING), + ...shorthands.padding('0', DIALOG_CONTENT_PADDING, '12px'), ...typographyStyles.body1, }, }); diff --git a/packages/react-components/react-dialog/src/components/DialogTitle/useDialogTitleStyles.ts b/packages/react-components/react-dialog/src/components/DialogTitle/useDialogTitleStyles.ts index d80561735ab8c..a7aaea823b1a7 100644 --- a/packages/react-components/react-dialog/src/components/DialogTitle/useDialogTitleStyles.ts +++ b/packages/react-components/react-dialog/src/components/DialogTitle/useDialogTitleStyles.ts @@ -30,6 +30,7 @@ const useStyles = makeStyles({ closeButton: { position: 'relative', lineHeight: '0', + cursor: 'pointer', }, closeButtonFocusIndicator: createFocusOutlineStyle(), // TODO: this should be extracted to another package diff --git a/packages/react-components/react-dialog/src/index.ts b/packages/react-components/react-dialog/src/index.ts index 88f615ccb9a93..f11d9d6d2ddc2 100644 --- a/packages/react-components/react-dialog/src/index.ts +++ b/packages/react-components/react-dialog/src/index.ts @@ -5,7 +5,7 @@ export { useDialogStyles_unstable, useDialog_unstable, } from './Dialog'; -export type { DialogProps, DialogSlots, DialogState } from './Dialog'; +export type { DialogProps, DialogOpenChangeListener, DialogSlots, DialogState } from './Dialog'; export { DialogTrigger, useDialogTrigger_unstable, renderDialogTrigger_unstable } from './DialogTrigger'; export type { diff --git a/packages/react-components/react-dialog/src/stories/DialogAlert.stories.tsx b/packages/react-components/react-dialog/src/stories/DialogAlert.stories.tsx new file mode 100644 index 0000000000000..63e2246aa5250 --- /dev/null +++ b/packages/react-components/react-dialog/src/stories/DialogAlert.stories.tsx @@ -0,0 +1,29 @@ +import * as React from 'react'; +import { Dialog, DialogTrigger, DialogContent, DialogTitle, DialogBody, DialogActions } from '@fluentui/react-dialog'; +import type { DialogProps } from '@fluentui/react-dialog'; +import { Button } from '@fluentui/react-components'; + +export const Alert = (props: Partial) => { + return ( + <> + + + + + + Alert dialog title + + This dialog cannot be dismissed by clicking on the overlay nor by pressing Escape. Close button should be + pressed to dismiss this Alert + + + + + + + + + + + ); +}; diff --git a/packages/react-components/react-dialog/src/stories/DialogChangeFocus.stories.tsx b/packages/react-components/react-dialog/src/stories/DialogChangeFocus.stories.tsx index dc0bfe3b25b24..e2bc57b73bed6 100644 --- a/packages/react-components/react-dialog/src/stories/DialogChangeFocus.stories.tsx +++ b/packages/react-components/react-dialog/src/stories/DialogChangeFocus.stories.tsx @@ -1,10 +1,17 @@ import * as React from 'react'; -import { Dialog, DialogTrigger, DialogContent, DialogTitle, DialogBody, DialogActions } from '@fluentui/react-dialog'; +import { + Dialog, + DialogOpenChangeListener, + DialogTrigger, + DialogContent, + DialogTitle, + DialogBody, + DialogActions, +} from '@fluentui/react-dialog'; import type { DialogProps } from '@fluentui/react-dialog'; import { Button } from '@fluentui/react-components'; import { makeStyles } from '@griffel/react'; -import { DialogOnOpenChange } from '../../Dialog'; const useStyles = makeStyles({ thirdAction: { @@ -19,10 +26,10 @@ export const ChangeFocus = (props: Partial) => { const styles = useStyles(); const buttonRef = React.useRef(null); const [open, setOpen] = React.useState(false); - const handleOpenChange: DialogOnOpenChange = (_, data) => setOpen(data.open); + const handleOpenChange: DialogOpenChangeListener = (_, data) => setOpen(data.open); React.useEffect(() => { - if (open) { - buttonRef.current?.focus(); + if (open && buttonRef.current) { + buttonRef.current.focus(); } }, [open]); return ( @@ -32,8 +39,8 @@ export const ChangeFocus = (props: Partial) => { - Dialog Title - Dialog Content + Dialog title + This dialog focus on the second button instead of first - Dialog Title - Dialog Content + Dialog title + + Lorem ipsum dolor sit amet consectetur adipisicing elit. Quisquam exercitationem cumque repellendus eaque + est dolor eius expedita nulla ullam? Tenetur reprehenderit aut voluptatum impedit voluptates in natus iure + cumque eaque? + diff --git a/packages/react-components/react-dialog/src/stories/DialogNested.stories.tsx b/packages/react-components/react-dialog/src/stories/DialogNested.stories.tsx index fec5aca3cbace..766ffd930519a 100644 --- a/packages/react-components/react-dialog/src/stories/DialogNested.stories.tsx +++ b/packages/react-components/react-dialog/src/stories/DialogNested.stories.tsx @@ -6,22 +6,22 @@ import { Button } from '@fluentui/react-components'; export const Nested = (props: Partial) => { return ( <> - + - Dialog Title + Dialog title - + - + - Inner Dialog Title + Inner dialog title - + diff --git a/packages/react-components/react-dialog/src/stories/DialogNonModal.stories.tsx b/packages/react-components/react-dialog/src/stories/DialogNonModal.stories.tsx index ebe61747223ae..a99c41aacc448 100644 --- a/packages/react-components/react-dialog/src/stories/DialogNonModal.stories.tsx +++ b/packages/react-components/react-dialog/src/stories/DialogNonModal.stories.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { Dialog, DialogTrigger, DialogContent, DialogTitle, DialogBody, DialogActions } from '@fluentui/react-dialog'; +import { Dialog, DialogTrigger, DialogContent, DialogTitle, DialogBody } from '@fluentui/react-dialog'; import type { DialogProps } from '@fluentui/react-dialog'; import { Button } from '@fluentui/react-components'; @@ -8,16 +8,15 @@ export const NonModal = (props: Partial) => { <> - + - Dialog Title - Dialog Content - - - - - + Non-modal dialog title + + Lorem, ipsum dolor sit amet consectetur adipisicing elit. Aliquid, explicabo repudiandae impedit doloribus + laborum quidem maxime dolores perspiciatis non ipsam, nostrum commodi quis autem sequi, incidunt cum? + Consequuntur, repellendus nostrum? + diff --git a/packages/react-components/react-dialog/src/stories/index.stories.tsx b/packages/react-components/react-dialog/src/stories/index.stories.tsx index 4d279c0dc3c64..74e32f42cf242 100644 --- a/packages/react-components/react-dialog/src/stories/index.stories.tsx +++ b/packages/react-components/react-dialog/src/stories/index.stories.tsx @@ -5,6 +5,7 @@ import bestPracticesMd from './DialogBestPractices.md'; export { Default } from './DialogDefault.stories'; export { NonModal } from './DialogNonModal.stories'; +export { Alert } from './DialogAlert.stories'; export { Nested } from './DialogNested.stories'; export { NoFocusableElement } from './DialogNoFocusableElement.stories'; export { ChangeFocus } from './DialogChangeFocus.stories'; From 86fbd4a05ea020e3c5156f61efa11933342457a2 Mon Sep 17 00:00:00 2001 From: Bernardo Sunderhus Date: Wed, 13 Jul 2022 18:11:13 +0000 Subject: [PATCH 17/17] Adds comments --- .../react-dialog/src/components/Dialog/Dialog.tsx | 7 +++++++ .../react-dialog/src/components/Dialog/Dialog.types.ts | 1 - .../react-dialog/src/components/Dialog/useDialog.ts | 4 +--- .../src/components/Dialog/useDialogStyles.ts | 5 ++++- .../src/components/DialogActions/DialogActions.tsx | 3 ++- .../src/components/DialogBody/DialogBody.tsx | 3 ++- .../components/DialogContent/useDialogContentStyles.ts | 1 + .../src/components/DialogTitle/DialogTitle.tsx | 4 +++- .../src/components/DialogTrigger/DialogTrigger.tsx | 10 ++++++++-- 9 files changed, 28 insertions(+), 10 deletions(-) diff --git a/packages/react-components/react-dialog/src/components/Dialog/Dialog.tsx b/packages/react-components/react-dialog/src/components/Dialog/Dialog.tsx index 3318843ba8ed3..6edfaedc645d3 100644 --- a/packages/react-components/react-dialog/src/components/Dialog/Dialog.tsx +++ b/packages/react-components/react-dialog/src/components/Dialog/Dialog.tsx @@ -5,6 +5,13 @@ import { useDialogStyles_unstable } from './useDialogStyles'; import type { DialogProps } from './Dialog.types'; import { useDialogContextValues_unstable } from './useDialogContextValues'; +/** + * 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: React.FC = React.memo(props => { const state = useDialog_unstable(props); const contextValues = useDialogContextValues_unstable(state); 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 19789da74e7a3..509f82d465ac7 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 @@ -75,5 +75,4 @@ export type DialogState = ComponentState & DialogContextValue & { content: React.ReactNode; trigger: React.ReactNode; - isSubDialog: boolean; }; 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 851ef5221c22a..137e66fbb9628 100644 --- a/packages/react-components/react-dialog/src/components/Dialog/useDialog.ts +++ b/packages/react-components/react-dialog/src/components/Dialog/useDialog.ts @@ -1,13 +1,12 @@ import * as React from 'react'; import { resolveShorthand, useControllableState, useEventCallback } from '@fluentui/react-utilities'; import type { DialogOpenChangeArgs, DialogProps, DialogState, DialogModalType } from './Dialog.types'; -import { DialogContext, DialogRequestOpenChangeData } from '../../contexts/dialogContext'; +import { DialogRequestOpenChangeData } from '../../contexts/dialogContext'; import { useFocusFinders } from '@fluentui/react-tabster'; import { useFluent_unstable } from '@fluentui/react-shared-contexts'; import { normalizeDefaultPrevented } from '../../utils/normalizeDefaultPrevented'; import { normalizeSetStateAction } from '../../utils/normalizeSetStateAction'; import { isEscapeKeyDismiss } from '../../utils/isEscapeKeyDown'; -import { useHasParentContext } from '@fluentui/react-context-selector'; /** * Create the state required to render Dialog. @@ -87,7 +86,6 @@ export const useDialog_unstable = (props: DialogProps): DialogState => { trigger, triggerRef, contentRef, - isSubDialog: useHasParentContext(DialogContext), requestOpenChange, }; }; diff --git a/packages/react-components/react-dialog/src/components/Dialog/useDialogStyles.ts b/packages/react-components/react-dialog/src/components/Dialog/useDialogStyles.ts index 6f22a5b5642c8..ab739db412c4b 100644 --- a/packages/react-components/react-dialog/src/components/Dialog/useDialogStyles.ts +++ b/packages/react-components/react-dialog/src/components/Dialog/useDialogStyles.ts @@ -1,5 +1,7 @@ import type { SlotClassNames } from '@fluentui/react-utilities'; import { makeStyles, mergeClasses, shorthands } from '@griffel/react'; +import { useHasParentContext } from '@fluentui/react-context-selector'; +import { DialogContext } from '../../contexts/dialogContext'; import type { DialogSlots, DialogState } from './Dialog.types'; export const dialogClassNames: SlotClassNames = { @@ -24,12 +26,13 @@ const useStyles = makeStyles({ */ export const useDialogStyles_unstable = (state: DialogState): DialogState => { const styles = useStyles(); + const isSubDialog = useHasParentContext(DialogContext); if (state.overlay) { state.overlay.className = mergeClasses( dialogClassNames.overlay, styles.overlay, - state.isSubDialog && styles.subDialogOverlay, + isSubDialog && styles.subDialogOverlay, state.overlay.className, ); } diff --git a/packages/react-components/react-dialog/src/components/DialogActions/DialogActions.tsx b/packages/react-components/react-dialog/src/components/DialogActions/DialogActions.tsx index 662152550980e..a835a297ffd09 100644 --- a/packages/react-components/react-dialog/src/components/DialogActions/DialogActions.tsx +++ b/packages/react-components/react-dialog/src/components/DialogActions/DialogActions.tsx @@ -6,7 +6,8 @@ import type { DialogActionsProps } from './DialogActions.types'; import type { ForwardRefComponent } from '@fluentui/react-utilities'; /** - * DialogActions component - TODO: add more docs + * `DialogActions` is a container for the actions of the dialog. + * Apart from styling, this component does not have other behavior. */ export const DialogActions: ForwardRefComponent = React.forwardRef((props, ref) => { const state = useDialogActions_unstable(props, ref); diff --git a/packages/react-components/react-dialog/src/components/DialogBody/DialogBody.tsx b/packages/react-components/react-dialog/src/components/DialogBody/DialogBody.tsx index 42f8d8c96bd9c..2cbdb20506fe5 100644 --- a/packages/react-components/react-dialog/src/components/DialogBody/DialogBody.tsx +++ b/packages/react-components/react-dialog/src/components/DialogBody/DialogBody.tsx @@ -6,7 +6,8 @@ import type { DialogBodyProps } from './DialogBody.types'; import type { ForwardRefComponent } from '@fluentui/react-utilities'; /** - * DialogBody component - TODO: add more docs + * The `DialogBody` is a container where the content of the dialog is rendered. + * Apart from styling, this component does not have other behavior. */ export const DialogBody: ForwardRefComponent = React.forwardRef((props, ref) => { const state = useDialogBody_unstable(props, ref); diff --git a/packages/react-components/react-dialog/src/components/DialogContent/useDialogContentStyles.ts b/packages/react-components/react-dialog/src/components/DialogContent/useDialogContentStyles.ts index 5699d6352f4ee..85586c02ff4b5 100644 --- a/packages/react-components/react-dialog/src/components/DialogContent/useDialogContentStyles.ts +++ b/packages/react-components/react-dialog/src/components/DialogContent/useDialogContentStyles.ts @@ -25,6 +25,7 @@ const useStyles = makeStyles({ maxHeight: '100vh', boxShadow: tokens.shadow64, backgroundColor: tokens.colorNeutralBackground1, + ...shorthands.border('1px', 'solid', tokens.colorTransparentStroke), ...shorthands.borderRadius('8px'), ...shorthands.margin('auto'), [MEDIA_QUERY_BREAKPOINT_SELECTOR]: { diff --git a/packages/react-components/react-dialog/src/components/DialogTitle/DialogTitle.tsx b/packages/react-components/react-dialog/src/components/DialogTitle/DialogTitle.tsx index 78be98b59105c..8286668919c2a 100644 --- a/packages/react-components/react-dialog/src/components/DialogTitle/DialogTitle.tsx +++ b/packages/react-components/react-dialog/src/components/DialogTitle/DialogTitle.tsx @@ -6,7 +6,9 @@ import type { DialogTitleProps } from './DialogTitle.types'; import type { ForwardRefComponent } from '@fluentui/react-utilities'; /** - * DialogTitle component - TODO: add more docs + * The `DialogTitle` component will expect to have a dialog title/header + * and will show the close (X icon) button if specified so. + * Apart from styling and presenting `closeButton`, this component does not have other behavior. */ export const DialogTitle: ForwardRefComponent = React.forwardRef((props, ref) => { const state = useDialogTitle_unstable(props, ref); diff --git a/packages/react-components/react-dialog/src/components/DialogTrigger/DialogTrigger.tsx b/packages/react-components/react-dialog/src/components/DialogTrigger/DialogTrigger.tsx index 82b3df92c6897..84b2a6b486339 100644 --- a/packages/react-components/react-dialog/src/components/DialogTrigger/DialogTrigger.tsx +++ b/packages/react-components/react-dialog/src/components/DialogTrigger/DialogTrigger.tsx @@ -5,8 +5,14 @@ import type { DialogTriggerProps } from './DialogTrigger.types'; import type { FluentTriggerComponent } from '@fluentui/react-utilities'; /** - * Wraps a trigger element as an only child - * and adds the necessary event handling to open a popup menu + * A non-visual component that wraps its child + * and configures them to be the trigger that will open or close a `Dialog`. + * This component should only accept one child. + * + * In case the trigger is used outside `Dialog` component + * it'll still provide basic ARIA related attributes + * to it's wrapped child, but it won't be able to alter the dialog `open` state anymore, + * in that case the user must provide a `controlled state` */ export const DialogTrigger: React.FC & FluentTriggerComponent = props => { const state = useDialogTrigger_unstable(props);