diff --git a/change/@ni-nimble-components-697d280f-9413-48ef-b2a4-0704abc5584c.json b/change/@ni-nimble-components-697d280f-9413-48ef-b2a4-0704abc5584c.json new file mode 100644 index 0000000000..38f18c98c5 --- /dev/null +++ b/change/@ni-nimble-components-697d280f-9413-48ef-b2a4-0704abc5584c.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "Clean up dialog/drawer properly if cancel event skipped", + "packageName": "@ni/nimble-components", + "email": "7282195+m-akinc@users.noreply.github.com", + "dependentChangeType": "patch" +} diff --git a/packages/nimble-components/src/dialog/index.ts b/packages/nimble-components/src/dialog/index.ts index 44398168c6..4b89ce1189 100644 --- a/packages/nimble-components/src/dialog/index.ts +++ b/packages/nimble-components/src/dialog/index.ts @@ -107,8 +107,7 @@ export class Dialog extends FoundationElement { throw new Error('Dialog is not open'); } this.dialogElement.close(); - this.resolveShow!(reason); - this.resolveShow = undefined; + this.doResolveShow(reason); } public slottedFooterElementsChanged( @@ -125,11 +124,33 @@ export class Dialog extends FoundationElement { if (this.preventDismiss) { event.preventDefault(); } else { - this.resolveShow!(UserDismissed); - this.resolveShow = undefined; + this.doResolveShow(UserDismissed); } return true; } + + /** + * @internal + */ + public closeHandler(): void { + if (this.resolveShow) { + // If + // - the browser implements dialogs with the CloseWatcher API, and + // - the user presses ESC without first interacting with the dialog (e.g. clicking, scrolling), + // the cancel event is not fired, but the close event still is, and the dialog just closes. + this.doResolveShow(UserDismissed); + } + } + + private doResolveShow(reason: CloseReason | UserDismissed): void { + if (!this.resolveShow) { + throw new Error( + 'Do not call doResolveShow unless there is a promise to resolve' + ); + } + this.resolveShow(reason); + this.resolveShow = undefined; + } } // eslint-disable-next-line @typescript-eslint/no-empty-interface diff --git a/packages/nimble-components/src/dialog/styles.ts b/packages/nimble-components/src/dialog/styles.ts index 653fcd6c0b..5d0b1c3c1c 100644 --- a/packages/nimble-components/src/dialog/styles.ts +++ b/packages/nimble-components/src/dialog/styles.ts @@ -15,7 +15,8 @@ import { elevation3BoxShadow, dialogSmallWidth, dialogSmallHeight, - dialogSmallMaxHeight + dialogSmallMaxHeight, + borderHoverColor } from '../theme-provider/design-tokens'; import { modalBackdropColorThemeColorStatic, @@ -25,6 +26,7 @@ import { import { Theme } from '../theme-provider/types'; import { themeBehavior } from '../utilities/style/theme'; import { accessiblyHidden } from '../utilities/style/accessibly-hidden'; +import { focusVisible } from '../utilities/style/focus'; export const styles = css` ${display('grid')} @@ -44,6 +46,10 @@ export const styles = css` display: flex; } + dialog${focusVisible} { + outline: 2px solid ${borderHoverColor}; + } + header { min-height: 48px; padding: 24px 24px 0px 24px; diff --git a/packages/nimble-components/src/dialog/template.ts b/packages/nimble-components/src/dialog/template.ts index d27899a530..23f2b63a57 100644 --- a/packages/nimble-components/src/dialog/template.ts +++ b/packages/nimble-components/src/dialog/template.ts @@ -8,6 +8,7 @@ export const template = html` role="dialog" part="control" @cancel="${(x, c) => x.cancelHandler(c.event)}" + @close="${x => x.closeHandler()}" aria-labelledby="header" >