@@ -9,6 +9,7 @@ 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 { modifier } from 'ember-modifier' ;
1213
1314import type { WithBoundArgs } from '@glint/template' ;
1415import type { HdsModalSizes , HdsModalColors } from './types.ts' ;
@@ -61,6 +62,7 @@ export default class HdsModal extends Component<HdsModalSignature> {
6162 private _element ! : HTMLDialogElement ;
6263 private _body ! : HTMLElement ;
6364 private _bodyInitialOverflowValue = '' ;
65+ private _clickOutsideToDismissHandler ! : ( event : MouseEvent ) => void ;
6466
6567 get isDismissDisabled ( ) : boolean {
6668 return this . args . isDismissDisabled ?? false ;
@@ -128,11 +130,33 @@ export default class HdsModal extends Component<HdsModalSignature> {
128130 }
129131 } else {
130132 this . _isOpen = false ;
133+
134+ // Reset page `overflow` property
135+ if ( this . _body ) {
136+ this . _body . style . removeProperty ( 'overflow' ) ;
137+ if ( this . _bodyInitialOverflowValue === '' ) {
138+ if ( this . _body . style . length === 0 ) {
139+ this . _body . removeAttribute ( 'style' ) ;
140+ }
141+ } else {
142+ this . _body . style . setProperty (
143+ 'overflow' ,
144+ this . _bodyInitialOverflowValue
145+ ) ;
146+ }
147+ }
148+
149+ // Return focus to a specific element (if provided)
150+ if ( this . args . returnFocusTo ) {
151+ const initiator = document . getElementById ( this . args . returnFocusTo ) ;
152+ if ( initiator ) {
153+ initiator . focus ( ) ;
154+ }
155+ }
131156 }
132157 }
133158
134- @action
135- didInsert ( element : HTMLDialogElement ) : void {
159+ private _registerDialog = modifier ( ( element : HTMLDialogElement ) => {
136160 // Store references of `<dialog>` and `<body>` elements
137161 this . _element = element ;
138162 this . _body = document . body ;
@@ -151,19 +175,43 @@ export default class HdsModal extends Component<HdsModalSignature> {
151175 if ( ! this . _element . open ) {
152176 this . open ( ) ;
153177 }
154- }
155178
156- @action
157- willDestroyNode ( ) : void {
158- if ( this . _element ) {
159- this . _element . removeEventListener (
179+ // Note: because the Modal has the `@isDismissedDisabled` argument, we need to add our own click outside to dismiss logic. This is because `ember-focus-trap` treats the `focusTrapOptions` as static, so we can't update it dynamically if `@isDismissDisabled` changes.
180+ this . _clickOutsideToDismissHandler = ( event : MouseEvent ) => {
181+ // check if the click is outside the modal and the modal is open
182+ if ( ! this . _element . contains ( event . target as Node ) && this . _isOpen ) {
183+ if ( ! this . isDismissDisabled ) {
184+ // here we use `void` because `onDismiss` is an async function, but in reality we don't need to handle the result or wait for its completion
185+ void this . onDismiss ( ) ;
186+ }
187+ }
188+ } ;
189+
190+ document . addEventListener ( 'click' , this . _clickOutsideToDismissHandler , {
191+ capture : true ,
192+ passive : false ,
193+ } ) ;
194+
195+ return ( ) => {
196+ // if the <dialog> is removed from the dom while open we emulate the close event
197+ if ( this . _isOpen ) {
198+ this . _element ?. dispatchEvent ( new Event ( 'close' ) ) ;
199+ }
200+
201+ this . _element ?. removeEventListener (
160202 'close' ,
161203 // eslint-disable-next-line @typescript-eslint/unbound-method
162204 this . registerOnCloseCallback ,
163205 true
164206 ) ;
165- }
166- }
207+
208+ document . removeEventListener (
209+ 'click' ,
210+ this . _clickOutsideToDismissHandler ,
211+ true
212+ ) ;
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