From c565449cbf24a89ee57804ec6bb34b00a2594f32 Mon Sep 17 00:00:00 2001 From: Bernardo Sunderhus Date: Wed, 7 Sep 2022 19:35:29 +0000 Subject: [PATCH] feat(react-dialog): 1st rule of ARIA for Dialog --- ...-dc310d89-6297-481f-a1fa-2a25450bc15a.json | 7 + .../react-components/react-dialog/Spec.md | 50 +++--- .../react-dialog/e2e/Dialog.e2e.tsx | 30 ++-- .../react-dialog/e2e/DialogTitle.e2e.tsx | 6 +- .../react-dialog/e2e/DialogTrigger.e2e.tsx | 6 +- .../react-dialog/etc/react-dialog.api.md | 21 ++- .../src/components/Dialog/Dialog.types.ts | 45 ++++-- .../src/components/Dialog/useDialog.ts | 17 ++- .../DialogSurface/DialogSurface.types.ts | 7 +- .../DialogSurface/useDialogSurface.ts | 73 ++++++++- .../DialogSurface/useDialogSurfaceStyles.ts | 16 +- .../src/contexts/dialogContext.ts | 5 +- .../react-dialog/src/index.ts | 9 +- .../DialogNoFocusableElement.stories.tsx | 2 +- .../react-dialog/src/utils/index.ts | 2 + .../react-dialog/src/utils/isEscapeKeyDown.ts | 15 +- .../src/utils/isHTMLDialogElement.ts | 23 +++ .../utils/useControlNativeDialogOpenState.ts | 24 +++ .../react-dialog/src/utils/useDialogProps.ts | 142 ++++++++++++++++++ .../src/utils/useFocusFirstElement.ts | 5 +- 20 files changed, 425 insertions(+), 80 deletions(-) create mode 100644 change/@fluentui-react-dialog-dc310d89-6297-481f-a1fa-2a25450bc15a.json create mode 100644 packages/react-components/react-dialog/src/utils/isHTMLDialogElement.ts create mode 100644 packages/react-components/react-dialog/src/utils/useControlNativeDialogOpenState.ts create mode 100644 packages/react-components/react-dialog/src/utils/useDialogProps.ts diff --git a/change/@fluentui-react-dialog-dc310d89-6297-481f-a1fa-2a25450bc15a.json b/change/@fluentui-react-dialog-dc310d89-6297-481f-a1fa-2a25450bc15a.json new file mode 100644 index 00000000000000..29e8d623c47f19 --- /dev/null +++ b/change/@fluentui-react-dialog-dc310d89-6297-481f-a1fa-2a25450bc15a.json @@ -0,0 +1,7 @@ +{ + "type": "prerelease", + "comment": "feat(react-dialog): 1st rule of ARIA for Dialog", + "packageName": "@fluentui/react-dialog", + "email": "bernardo.sunderhus@gmail.com", + "dependentChangeType": "patch" +} diff --git a/packages/react-components/react-dialog/Spec.md b/packages/react-components/react-dialog/Spec.md index b5b2d81e7566bf..033c44a88b68bb 100644 --- a/packages/react-components/react-dialog/Spec.md +++ b/packages/react-components/react-dialog/Spec.md @@ -83,19 +83,7 @@ Sample usages will be give in the following section of this document [Sample cod The 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 `DialogSurface` or a `DialogTrigger` and a `DialogSurface` (or some component that will eventually render one of those compound components) in this specific order ```tsx -type DialogSlots = { - /** - * Dimmed background of dialog. - * The default backdrop is rendered as a `
` with styling. - * This slot expects a `
` element which will replace the default backdrop. - * The backdrop should have `aria-hidden="true"`. - */ - backdrop?: Slot<'div'>; - /** - * The root element of the Dialog right after Portal. - */ - root: Slot<'div'>; -}; +type DialogSlots = {}; type DialogProps = ComponentProps & { /** @@ -167,14 +155,21 @@ export type DialogTriggerProps = { The `DialogSurface` component represents the visual part of a `Dialog` as a whole, it contains everything that should be visible. ```tsx -type DialogTitleSlots = { +type DialogSurfaceSlots = { /** - * By default this is a div. + * Dimmed background of dialog. + * The default backdrop is rendered as a `
` with styling. + * This slot expects a `
` element which will replace the default backdrop. + * The backdrop should have `aria-hidden="true"`. + * + * By default if `DialogSurface` is `` element the backdrop is ignored, + * since native `` element supports [::backdrop](https://developer.mozilla.org/en-US/docs/Web/CSS/::backdrop) */ - root: Slot<'div', 'main'>; + backdrop?: Slot<'div'>; + root: NonNullable>; }; -type DialogTitleProps = ComponentProps; +type DialogTitleProps = ComponentProps; ``` ### DialogTitle @@ -405,6 +400,27 @@ function AsyncConfirmDialog() { } ``` +### Opting out of native `` + +```tsx +const dialog = + + + + + This is as basic as it gets. + + +``` + +```html + + + + + +``` + ## Migration _TBA: Link to migration guide doc_ diff --git a/packages/react-components/react-dialog/e2e/Dialog.e2e.tsx b/packages/react-components/react-dialog/e2e/Dialog.e2e.tsx index b70e511c07d1fa..b7db512ed4829e 100644 --- a/packages/react-components/react-dialog/e2e/Dialog.e2e.tsx +++ b/packages/react-components/react-dialog/e2e/Dialog.e2e.tsx @@ -63,7 +63,7 @@ describe('Dialog', () => { , ); - cy.get(dialogTriggerOpenSelector).click(); + cy.get(dialogTriggerOpenSelector).realClick(); cy.get(dialogSurfaceSelector).should('exist'); }); it('should focus on first focusabled element when opened', () => { @@ -90,7 +90,7 @@ describe('Dialog', () => { , ); - cy.get(dialogTriggerOpenSelector).click(); + cy.get(dialogTriggerOpenSelector).realClick(); cy.get(dialogTriggerCloseSelector).should('be.focused'); }); it('should focus on body if no focusabled element in dialog', () => { @@ -109,7 +109,7 @@ describe('Dialog', () => { , ); - cy.get(dialogTriggerOpenSelector).click(); + cy.get(dialogTriggerOpenSelector).realClick(); cy.focused().should('not.exist'); }); it('should focus back on trigger when dialog closed', () => { @@ -136,8 +136,8 @@ describe('Dialog', () => { , ); - cy.get(dialogTriggerOpenSelector).click(); - cy.get(dialogTriggerCloseSelector).click(); + cy.get(dialogTriggerOpenSelector).realClick(); + cy.get(dialogTriggerCloseSelector).realClick(); cy.get(dialogTriggerOpenSelector).should('be.focused'); }); it('should allow change of focus on open', () => { @@ -173,7 +173,7 @@ describe('Dialog', () => { ); }; mount(); - cy.get(dialogTriggerOpenSelector).click(); + cy.get(dialogTriggerOpenSelector).realClick(); cy.get(dialogTriggerCloseSelector).should('be.focused'); }); describe('modalType = modal', () => { @@ -199,8 +199,8 @@ describe('Dialog', () => { , ); - cy.get(dialogTriggerOpenSelector).click(); - cy.focused().type('{esc}'); + cy.get(dialogTriggerOpenSelector).realClick(); + cy.focused().realType('{esc}'); cy.get(dialogSurfaceSelector).should('not.exist'); }); it('should lock body scroll when dialog open', () => { @@ -225,7 +225,7 @@ describe('Dialog', () => { , ); - cy.get(dialogTriggerOpenSelector).click(); + cy.get(dialogTriggerOpenSelector).realClick(); cy.get('body').should('have.css', 'overflow', 'hidden'); }); }); @@ -252,8 +252,8 @@ describe('Dialog', () => { , ); - cy.get(dialogTriggerOpenSelector).click(); - cy.focused().type('{esc}'); + cy.get(dialogTriggerOpenSelector).realClick(); + cy.focused().realType('{esc}'); cy.get(dialogSurfaceSelector).should('not.exist'); }); it('should not lock body scroll when dialog open', () => { @@ -278,7 +278,7 @@ describe('Dialog', () => { , ); - cy.get(dialogTriggerOpenSelector).click(); + cy.get(dialogTriggerOpenSelector).realClick(); cy.get('body').should('not.have.css', 'overflow', 'hidden'); }); }); @@ -305,8 +305,8 @@ describe('Dialog', () => { , ); - cy.get(dialogTriggerOpenSelector).click(); - cy.focused().type('{esc}'); + cy.get(dialogTriggerOpenSelector).realClick(); + cy.focused().realType('{esc}'); cy.get(dialogSurfaceSelector).should('exist'); }); it('should lock body scroll when dialog open', () => { @@ -331,7 +331,7 @@ describe('Dialog', () => { , ); - cy.get(dialogTriggerOpenSelector).click(); + cy.get(dialogTriggerOpenSelector).realClick(); cy.get('body').should('have.css', 'overflow', 'hidden'); }); }); diff --git a/packages/react-components/react-dialog/e2e/DialogTitle.e2e.tsx b/packages/react-components/react-dialog/e2e/DialogTitle.e2e.tsx index 670b5771a5fa74..9ba2a5175c2e65 100644 --- a/packages/react-components/react-dialog/e2e/DialogTitle.e2e.tsx +++ b/packages/react-components/react-dialog/e2e/DialogTitle.e2e.tsx @@ -34,7 +34,7 @@ describe('DialogTitle', () => { , ); - cy.get(dialogTriggerOpenSelector).click(); + cy.get(dialogTriggerOpenSelector).realClick(); cy.get(dialogActionSelector).should('not.exist'); }); }); @@ -61,7 +61,7 @@ describe('DialogTitle', () => { , ); - cy.get(dialogTriggerOpenSelector).click(); + cy.get(dialogTriggerOpenSelector).realClick(); cy.get(dialogActionSelector).should('exist'); }); }); @@ -88,7 +88,7 @@ describe('DialogTitle', () => { , ); - cy.get(dialogTriggerOpenSelector).click(); + cy.get(dialogTriggerOpenSelector).realClick(); cy.get(dialogActionSelector).should('not.exist'); }); }); diff --git a/packages/react-components/react-dialog/e2e/DialogTrigger.e2e.tsx b/packages/react-components/react-dialog/e2e/DialogTrigger.e2e.tsx index 1daa0721cb9612..2fe6b03e7441eb 100644 --- a/packages/react-components/react-dialog/e2e/DialogTrigger.e2e.tsx +++ b/packages/react-components/react-dialog/e2e/DialogTrigger.e2e.tsx @@ -33,7 +33,7 @@ describe('DialogTrigger', () => { , ); - cy.get(dialogTriggerOpenSelector).click(); + cy.get(dialogTriggerOpenSelector).realClick(); cy.get(dialogSurfaceSelector).should('not.exist'); }); it(`should open dialog when 'aria-disabled' is false`, () => { @@ -58,7 +58,7 @@ describe('DialogTrigger', () => { , ); - cy.get(dialogTriggerOpenSelector).click(); + cy.get(dialogTriggerOpenSelector).realClick(); cy.get(dialogSurfaceSelector).should('exist'); }); it('should work with any element besides