diff --git a/change/@fluentui-react-dialog-dd73c40d-e260-467e-821b-c08ad73287bf.json b/change/@fluentui-react-dialog-dd73c40d-e260-467e-821b-c08ad73287bf.json new file mode 100644 index 00000000000000..ea2c53be90f70d --- /dev/null +++ b/change/@fluentui-react-dialog-dd73c40d-e260-467e-821b-c08ad73287bf.json @@ -0,0 +1,7 @@ +{ + "type": "minor", + "comment": "feat: allows user to opt-out of uncontrolled open changes", + "packageName": "@fluentui/react-dialog", + "email": "bernardo.sunderhus@gmail.com", + "dependentChangeType": "patch" +} 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 c12aa430a27f7a..a7f6ec5de78294 100644 --- a/packages/react-components/react-dialog/etc/react-dialog.api.md +++ b/packages/react-components/react-dialog/etc/react-dialog.api.md @@ -112,7 +112,7 @@ export type DialogOpenChangeData = { export type DialogOpenChangeEvent = DialogOpenChangeData['event']; // @public -export type DialogOpenChangeEventHandler = (event: DialogOpenChangeEvent, data: DialogOpenChangeData) => void; +export type DialogOpenChangeEventHandler = (event: DialogOpenChangeEvent, data: DialogOpenChangeData) => void | boolean; // @public (undocumented) export type DialogProps = ComponentProps> & { diff --git a/packages/react-components/react-dialog/src/components/Dialog/Dialog.cy.tsx b/packages/react-components/react-dialog/src/components/Dialog/Dialog.cy.tsx index 0bedc2e47b1846..4668d3789bb08c 100644 --- a/packages/react-components/react-dialog/src/components/Dialog/Dialog.cy.tsx +++ b/packages/react-components/react-dialog/src/components/Dialog/Dialog.cy.tsx @@ -1,3 +1,6 @@ +/// +/// + import * as React from 'react'; import { mount as mountBase } from '@cypress/react'; @@ -659,4 +662,40 @@ describe('Dialog', () => { cy.get('#second-dialog').should('not.exist'); cy.get('#first-dialog').should('not.exist'); }); + it('should be able to stop uncontrolled dialog from closing', () => { + mount( + { + if (data.type === 'escapeKeyDown') { + return false; + } + }} + > + + + + + + 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? + + + + + + + + + + , + ); + cy.get(dialogTriggerOpenSelector).realClick(); + cy.focused().realType('{esc}'); + cy.get(dialogSurfaceSelector).should('exist'); + }); }); 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 6a05e1fe8576df..309c6e8e61dc66 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 @@ -29,11 +29,15 @@ export type DialogModalType = 'modal' | 'non-modal' | 'alert'; /** * Callback fired when the component changes value from open state. * + * If a boolean is returned, it will be used to indicate if the opening should be allowed. + * If false, the opening will be prevented. + * If true or undefined, the opening will be allowed. + * * @param event - a React's Synthetic event or a KeyboardEvent in case of `documentEscapeKeyDown` * @param data - A data object with relevant information, * such as open value and type of interaction that created the event */ -export type DialogOpenChangeEventHandler = (event: DialogOpenChangeEvent, data: DialogOpenChangeData) => void; +export type DialogOpenChangeEventHandler = (event: DialogOpenChangeEvent, data: DialogOpenChangeData) => void | boolean; export type DialogContextValues = { dialog: DialogContextValue; 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 b08036b9ca834c..b70a68ec62200a 100644 --- a/packages/react-components/react-dialog/src/components/Dialog/useDialog.ts +++ b/packages/react-components/react-dialog/src/components/Dialog/useDialog.ts @@ -27,11 +27,11 @@ export const useDialog_unstable = (props: DialogProps): DialogState => { }); const requestOpenChange = useEventCallback((data: DialogOpenChangeData) => { - onOpenChange?.(data.event, data); + const isInternalsAllowed = onOpenChange?.(data.event, data) ?? true; // if user prevents default then do not change state value // otherwise updates state value and trigger reference to the element that caused the opening - if (!data.event.isDefaultPrevented()) { + if (isInternalsAllowed && !data.event.isDefaultPrevented()) { setOpen(data.open); } });