@@ -9,8 +9,11 @@ import { action } from '@ember/object';
99import { assert } from '@ember/debug' ;
1010import { getElementId } from '../../../utils/hds-get-element-id.ts' ;
1111import { buildWaiter } from '@ember/test-waiters' ;
12+ import { registerDestructor } from '@ember/destroyable' ;
13+ import { modifier } from 'ember-modifier' ;
1214
1315import type { WithBoundArgs } from '@glint/template' ;
16+ import type Owner from '@ember/owner' ;
1417import type { HdsModalSizes , HdsModalColors } from './types.ts' ;
1518
1619import HdsDialogPrimitiveHeaderComponent from '../dialog-primitive/header.ts' ;
@@ -61,6 +64,27 @@ export default class HdsModal extends Component<HdsModalSignature> {
6164 private _element ! : HTMLDialogElement ;
6265 private _body ! : HTMLElement ;
6366 private _bodyInitialOverflowValue = '' ;
67+ private _clickHandler ! : ( event : MouseEvent ) => void ;
68+
69+ constructor ( owner : Owner , args : HdsModalSignature [ 'Args' ] ) {
70+ super ( owner , args ) ;
71+
72+ registerDestructor ( this , ( ) : void => {
73+ // if the <dialog> is removed from the dom while open we emulate the close event
74+ if ( this . _element && this . _isOpen ) {
75+ this . _element . dispatchEvent ( new Event ( 'close' ) ) ;
76+
77+ this . _element . removeEventListener (
78+ 'close' ,
79+ // eslint-disable-next-line @typescript-eslint/unbound-method
80+ this . registerOnCloseCallback ,
81+ true
82+ ) ;
83+ }
84+
85+ document . removeEventListener ( 'click' , this . _clickHandler , true ) ;
86+ } ) ;
87+ }
6488
6589 get isDismissDisabled ( ) : boolean {
6690 return this . args . isDismissDisabled ?? false ;
@@ -128,11 +152,33 @@ export default class HdsModal extends Component<HdsModalSignature> {
128152 }
129153 } else {
130154 this . _isOpen = false ;
155+
156+ // Reset page `overflow` property
157+ if ( this . _body ) {
158+ this . _body . style . removeProperty ( 'overflow' ) ;
159+ if ( this . _bodyInitialOverflowValue === '' ) {
160+ if ( this . _body . style . length === 0 ) {
161+ this . _body . removeAttribute ( 'style' ) ;
162+ }
163+ } else {
164+ this . _body . style . setProperty (
165+ 'overflow' ,
166+ this . _bodyInitialOverflowValue
167+ ) ;
168+ }
169+ }
170+
171+ // Return focus to a specific element (if provided)
172+ if ( this . args . returnFocusTo ) {
173+ const initiator = document . getElementById ( this . args . returnFocusTo ) ;
174+ if ( initiator ) {
175+ initiator . focus ( ) ;
176+ }
177+ }
131178 }
132179 }
133180
134- @action
135- didInsert ( element : HTMLDialogElement ) : void {
181+ private _registerDialog = modifier ( ( element : HTMLDialogElement ) => {
136182 // Store references of `<dialog>` and `<body>` elements
137183 this . _element = element ;
138184 this . _body = document . body ;
@@ -151,19 +197,21 @@ export default class HdsModal extends Component<HdsModalSignature> {
151197 if ( ! this . _element . open ) {
152198 this . open ( ) ;
153199 }
154- }
155200
156- @action
157- willDestroyNode ( ) : void {
158- if ( this . _element ) {
159- this . _element . removeEventListener (
160- 'close' ,
161- // eslint-disable-next-line @typescript-eslint/unbound-method
162- this . registerOnCloseCallback ,
163- true
164- ) ;
165- }
166- }
201+ this . _clickHandler = ( event : MouseEvent ) => {
202+ // check if the click is outside the modal and the modal is open
203+ if ( ! this . _element . contains ( event . target as Node ) && this . _isOpen ) {
204+ if ( ! this . isDismissDisabled ) {
205+ void this . onDismiss ( ) ;
206+ }
207+ }
208+ } ;
209+
210+ document . addEventListener ( 'click' , this . _clickHandler , {
211+ capture : true ,
212+ passive : false ,
213+ } ) ;
214+ } ) ;
167215
168216 @action
169217 open ( ) : void {
@@ -185,7 +233,6 @@ export default class HdsModal extends Component<HdsModalSignature> {
185233 async onDismiss ( ) : Promise < void > {
186234 // allow ember test helpers to be aware of when the `close` event fires
187235 // when using `click` or other helpers from '@ember/test-helpers'
188- // Notice: this code will get stripped out in production builds (DEBUG evaluates to `true` in dev/test builds, but `false` in prod builds)
189236 if ( this . _element . open ) {
190237 const token = waiter . beginAsync ( ) ;
191238 const listener = ( ) => {
@@ -197,28 +244,5 @@ export default class HdsModal extends Component<HdsModalSignature> {
197244
198245 // Make modal dialog invisible using the native `close` method
199246 this . _element . close ( ) ;
200-
201- // Reset page `overflow` property
202- if ( this . _body ) {
203- this . _body . style . removeProperty ( 'overflow' ) ;
204- if ( this . _bodyInitialOverflowValue === '' ) {
205- if ( this . _body . style . length === 0 ) {
206- this . _body . removeAttribute ( 'style' ) ;
207- }
208- } else {
209- this . _body . style . setProperty (
210- 'overflow' ,
211- this . _bodyInitialOverflowValue
212- ) ;
213- }
214- }
215-
216- // Return focus to a specific element (if provided)
217- if ( this . args . returnFocusTo ) {
218- const initiator = document . getElementById ( this . args . returnFocusTo ) ;
219- if ( initiator ) {
220- initiator . focus ( ) ;
221- }
222- }
223247 }
224248}
0 commit comments