@@ -31,118 +31,107 @@ export default class Modal extends Component<Props, State> {
3131 shown : false ,
3232 } ;
3333
34- private modal ?: HTMLElement ;
35- private returnFocusElement ?: HTMLElement | null ;
34+ private modal ?: HTMLDialogElement ;
35+
36+ componentDidMount ( ) {
37+ // Once a transition ends, check if the modal should be closed (not just hidden)
38+ // dialog.close() instantly hides the modal, so we call it AFTER fading it out i.e. on transition end
39+ this . modal ?. addEventListener (
40+ 'transitionend' ,
41+ this . _closeOnTransitionEnd . bind ( this ) ,
42+ ) ;
43+ this . modal ?. setAttribute ( 'inert' , 'enabled' ) ;
44+ }
3645
46+ private _closeOnTransitionEnd ( ) {
47+ // If modal does not exist
48+ // Or if it's not being closed at the moment
49+ if ( ! this . modal || ! this . modal . classList . contains ( style . modalClosing ) )
50+ return ;
51+
52+ this . modal . close ( ) ;
53+ this . modal . classList . remove ( style . modalClosing ) ;
54+ this . modal . setAttribute ( 'inert' , 'enabled' ) ;
55+ }
56+
57+ /**
58+ * Function to set up the modal and show it
59+ */
3760 showModal ( message : ModalMessage ) {
38- if ( this . state . shown ) return ;
3961 if ( ! this . modal ) return ;
4062
41- // Set element to return focus to after hiding
42- this . returnFocusElement = document . activeElement as HTMLElement ;
43-
44- this . modal . style . display = '' ;
4563 this . setState ( {
4664 message : message ,
4765 shown : true ,
4866 } ) ;
49- // Wait for the 'display' reset to take place, then focus
50- setTimeout ( ( ) => {
51- this . modal ?. querySelector ( 'button' ) ?. focus ( ) ;
52- } , 0 ) ;
67+
68+ // Actually show the modal
69+ this . modal . removeAttribute ( 'inert' ) ;
70+ this . modal . showModal ( ) ;
5371 }
5472
73+ /**
74+ * Function to hide the modal with a fade-out transition
75+ * Adds the `modal--closing` class which is removed on transition end
76+ */
5577 hideModal ( ) {
78+ if ( ! this . modal || ! this . modal . open ) return ;
79+
80+ // Make the modal fade out
81+ this . modal . classList . add ( style . modalClosing ) ;
82+
5683 this . setState ( {
5784 message : { ...this . state . message } ,
5885 shown : false ,
5986 } ) ;
60- setTimeout ( ( ) => {
61- this . modal && ( this . modal . style . display = 'none' ) ;
62- this . returnFocusElement ?. focus ( ) ;
63- } , 250 ) ;
64- }
65-
66- private _getCloseButton ( ) {
67- return this . modal ! . querySelector ( 'button' ) ! ;
68- }
69-
70- private _getLastFocusable ( ) {
71- const focusables = this . modal ! . querySelectorAll ( 'button, a' ) ;
72- return focusables [ focusables . length - 1 ] as HTMLElement ;
7387 }
7488
7589 private _onKeyDown ( e : KeyboardEvent ) {
76- // If Escape, hide modal
90+ // Default behaviour of <dialog> closes it instantly when you press Esc
91+ // So we hijack it to smoothly hide the modal
7792 if ( e . key === 'Escape' || e . keyCode == 27 ) {
7893 this . hideModal ( ) ;
7994 e . preventDefault ( ) ;
8095 e . stopImmediatePropagation ( ) ;
81- return ;
82- }
83-
84- let isTabPressed = e . key === 'Tab' || e . keyCode === 9 ;
85-
86- if ( ! isTabPressed ) return ;
87-
88- if ( e . shiftKey ) {
89- // If SHIFT + TAB was pressed on the first focusable element
90- // Move focus to the last focusable element
91- if ( document . activeElement === this . _getCloseButton ( ) ) {
92- this . _getLastFocusable ( ) . focus ( ) ;
93- e . preventDefault ( ) ;
94- e . stopImmediatePropagation ( ) ;
95- }
96- } else {
97- // If TAB was pressed on the last focusable element
98- // Move focus to the first focusable element
99- if ( document . activeElement === this . _getLastFocusable ( ) ) {
100- this . _getCloseButton ( ) . focus ( ) ;
101- e . preventDefault ( ) ;
102- e . stopImmediatePropagation ( ) ;
103- }
10496 }
10597 }
10698
10799 render ( { } : Props , { message, shown } : State ) {
108100 return (
109- < div
110- class = { ` ${ style . modalOverlay } ${ shown && style . modalShown } ` }
101+ < dialog
102+ ref = { linkRef ( this , 'modal' ) }
111103 onKeyDown = { ( e ) => this . _onKeyDown ( e ) }
112- tabIndex = { shown ? 0 : - 1 }
113104 >
114- < div class = { style . modal } ref = { linkRef ( this , 'modal' ) } >
115- < header class = { style . header } >
116- < span class = { style . modalTypeIcon } >
117- { message . type === 'info' ? (
118- < InfoIcon />
119- ) : message . type === 'error' ? (
120- < ModalErrorIcon />
121- ) : (
122- < DiamondStarIcon />
123- ) }
124- </ span >
125- < span class = { style . modalTitle } > { message . title } </ span >
126- < button class = { style . closeButton } onClick = { ( ) => this . hideModal ( ) } >
127- < svg viewBox = "0 0 480 480" fill = "currentColor" >
128- < path
129- d = "M119.356 120L361 361M360.644 120L119 361"
130- stroke = "#fff"
131- stroke-width = "37"
132- stroke-linecap = "round"
133- />
134- </ svg >
135- </ button >
136- </ header >
137- < div class = { style . contentContainer } >
138- < article
139- class = { style . content }
140- dangerouslySetInnerHTML = { { __html : message . content } }
141- > </ article >
142- </ div >
143- < footer class = { style . footer } > </ footer >
105+ < header class = { style . header } >
106+ < span class = { style . modalTypeIcon } >
107+ { message . type === 'info' ? (
108+ < InfoIcon />
109+ ) : message . type === 'error' ? (
110+ < ModalErrorIcon />
111+ ) : (
112+ < DiamondStarIcon />
113+ ) }
114+ </ span >
115+ < span class = { style . modalTitle } > { message . title } </ span >
116+ < button class = { style . closeButton } onClick = { ( ) => this . hideModal ( ) } >
117+ < svg viewBox = "0 0 480 480" fill = "currentColor" >
118+ < path
119+ d = "M119.356 120L361 361M360.644 120L119 361"
120+ stroke = "#fff"
121+ stroke-width = "37"
122+ stroke-linecap = "round"
123+ />
124+ </ svg >
125+ </ button >
126+ </ header >
127+ < div class = { style . contentContainer } >
128+ < article
129+ class = { style . content }
130+ dangerouslySetInnerHTML = { { __html : message . content } }
131+ > </ article >
144132 </ div >
145- </ div >
133+ < footer class = { style . footer } > </ footer >
134+ </ dialog >
146135 ) ;
147136 }
148137}
0 commit comments