1- import React , { useCallback , useEffect , useRef , useState } from 'react'
1+ import React , { useCallback , useRef } from 'react'
22import styled from 'styled-components'
33import Button , { ButtonPrimary , ButtonDanger , ButtonProps } from '../deprecated/Button'
44import Box from '../Box'
55import { get } from '../constants'
6- import { useOnEscapePress , useProvidedRefOrCreate } from '../hooks'
7- import { useFocusTrap } from '../hooks/useFocusTrap'
86import sx , { SxProp } from '../sx'
97import Octicon from '../Octicon'
108import { XIcon } from '@primer/octicons-react'
119import { useFocusZone } from '../hooks/useFocusZone'
1210import { FocusKeys } from '@primer/behaviors'
13- import Portal from '../Portal'
1411import { useRefObjectAsForwardedRef } from '../hooks/useRefObjectAsForwardedRef'
1512import { useId } from '../hooks/useId'
13+ import useIsomorphicLayoutEffect from '../utils/useIsomorphicLayoutEffect'
1614
1715/* Dialog Version 2 */
1816
@@ -95,13 +93,23 @@ export interface DialogProps extends SxProp {
9593 footerButtons ?: DialogButtonProps [ ]
9694
9795 /**
98- * This method is invoked when a gesture to close the dialog is used (either
99- * an Escape key press or clicking the "X" in the top-right corner). The
100- * gesture argument indicates the gesture that was used to close the dialog
101- * (either 'close-button' or 'escape').
96+ * This method is invoked when the dialog has been closed. This could
97+ * be from the Dialog.
98+ * @param gesture - Deprecated: The gesture argument was used to
99+ * indicate if the gesture was from the close-button or escape button.
100+ * It will always be `'close-button'`, to check for _cancelations_ of
101+ * the dialog using the Esc key, use onCancel.
102102 */
103103 onClose : ( gesture : 'close-button' | 'escape' ) => void
104104
105+ /**
106+ * This method is invoked when the user instructs the browser they wish to
107+ * dismiss the dialog. Typically this means they have pressed the `Esc` key.
108+ *
109+ * @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLDialogElement/cancel_event
110+ */
111+ onCancel ?: ( ) => void
112+
105113 /**
106114 * Default: "dialog". The ARIA role to assign to this dialog.
107115 * @see https://www.w3.org/TR/wai-aria-practices-1.1/#dialog_modal
@@ -125,6 +133,11 @@ export interface DialogProps extends SxProp {
125133 * auto: variable based on contents
126134 */
127135 height ?: DialogHeight
136+
137+ /**
138+ * Whether or not the Dialog is rendered initially open
139+ */
140+ initiallyOpen ?: boolean
128141}
129142
130143/**
@@ -144,32 +157,11 @@ export interface DialogHeaderProps extends DialogProps {
144157 dialogDescriptionId : string
145158}
146159
147- const Backdrop = styled ( 'div' ) `
148- position: fixed;
149- top: 0;
150- left: 0;
151- bottom: 0;
152- right: 0;
153- display: flex;
154- align-items: center;
155- justify-content: center;
156- background-color: ${ get ( 'colors.primer.canvas.backdrop' ) } ;
157- animation: dialog-backdrop-appear ${ ANIMATION_DURATION } ${ get ( 'animation.easeOutCubic' ) } ;
158-
159- @keyframes dialog-backdrop-appear {
160- 0% {
161- opacity: 0;
162- }
163- 100% {
164- opacity: 1;
165- }
166- }
167- `
168-
169160const heightMap = {
170161 small : '480px' ,
171162 large : '640px' ,
172163 auto : 'auto' ,
164+ 'min-content' : 'min-content' ,
173165} as const
174166
175167const widthMap = {
@@ -187,19 +179,40 @@ type StyledDialogProps = {
187179 height ?: DialogHeight
188180} & SxProp
189181
190- const StyledDialog = styled . div < StyledDialogProps > `
191- display: flex;
182+ const StyledDialog = styled . dialog < StyledDialogProps > `
192183 flex-direction: column;
193184 background-color: ${ get ( 'colors.canvas.overlay' ) } ;
185+ color: ${ get ( 'colors.fg.default' ) } ;
194186 box-shadow: ${ get ( 'shadows.overlay.shadow' ) } ;
195187 min-width: 296px;
196188 max-width: calc(100vw - 64px);
197189 max-height: calc(100vh - 64px);
198190 width: ${ props => widthMap [ props . width ?? ( 'xlarge' as const ) ] } ;
199191 height: ${ props => heightMap [ props . height ?? ( 'auto' as const ) ] } ;
192+ border: 0;
200193 border-radius: 12px;
194+ padding: 0;
201195 opacity: 1;
202196 animation: overlay--dialog-appear ${ ANIMATION_DURATION } ${ get ( 'animation.easeOutCubic' ) } ;
197+ overflow: initial;
198+
199+ &[open] {
200+ display: flex;
201+ }
202+
203+ &::backdrop {
204+ background-color: ${ get ( 'colors.primer.canvas.backdrop' ) } ;
205+ animation: dialog-backdrop-appear ${ ANIMATION_DURATION } ${ get ( 'animation.easeOutCubic' ) } ;
206+ }
207+
208+ @keyframes dialog-backdrop-appear {
209+ 0% {
210+ opacity: 0;
211+ }
212+ 100% {
213+ opacity: 1;
214+ }
215+ }
203216
204217 @keyframes overlay--dialog-appear {
205218 0% {
@@ -252,84 +265,56 @@ const DefaultFooter: React.FC<React.PropsWithChildren<DialogProps>> = ({footerBu
252265 ) : null
253266}
254267
255- const _Dialog = React . forwardRef < HTMLDivElement , React . PropsWithChildren < DialogProps > > ( ( props , forwardedRef ) => {
268+ const _Dialog = React . forwardRef < HTMLDialogElement , React . PropsWithChildren < DialogProps > > ( ( props , forwardedRef ) => {
256269 const {
257270 title = 'Dialog' ,
258271 subtitle = '' ,
259272 renderHeader,
260273 renderBody,
261274 renderFooter,
262275 onClose,
276+ onCancel,
263277 role = 'dialog' ,
264278 width = 'xlarge' ,
265279 height = 'auto' ,
266- footerButtons = [ ] ,
280+ initiallyOpen = true ,
267281 sx,
268282 } = props
269283 const dialogLabelId = useId ( )
270284 const dialogDescriptionId = useId ( )
271- const autoFocusedFooterButtonRef = useRef < HTMLButtonElement > ( null )
272- for ( const footerButton of footerButtons ) {
273- if ( footerButton . autoFocus ) {
274- footerButton . ref = autoFocusedFooterButtonRef
275- }
276- }
277285 const defaultedProps = { ...props , title, subtitle, role, dialogLabelId, dialogDescriptionId}
278286
279- const dialogRef = useRef < HTMLDivElement > ( null )
287+ const dialogRef = useRef < HTMLDialogElement > ( null )
280288 useRefObjectAsForwardedRef ( forwardedRef , dialogRef )
281- const backdropRef = useRef < HTMLDivElement > ( null )
282- useFocusTrap ( { containerRef : dialogRef , restoreFocusOnCleanUp : true , initialFocusRef : autoFocusedFooterButtonRef } )
283-
284- useOnEscapePress (
285- ( event : KeyboardEvent ) => {
286- onClose ( 'escape' )
287- event . preventDefault ( )
288- } ,
289- [ onClose ] ,
290- )
291289
292- React . useEffect ( ( ) => {
293- const bodyOverflowStyle = document . body . style . overflow || ''
294- // If the body is already set to overflow: hidden, it likely means
295- // that there is already a modal open. In that case, we should bail
296- // so we don't re-enable scroll after the second dialog is closed.
297- if ( bodyOverflowStyle === 'hidden' ) {
298- return
290+ useIsomorphicLayoutEffect ( ( ) => {
291+ if ( initiallyOpen && ! dialogRef . current ?. open && dialogRef . current ?. isConnected ) {
292+ dialogRef . current . showModal ( )
299293 }
294+ } , [ initiallyOpen , dialogRef ] )
300295
301- document . body . style . overflow = 'hidden'
302-
303- return ( ) => {
304- document . body . style . overflow = bodyOverflowStyle
305- }
306- } , [ ] )
296+ const onCloseHandler = useCallback ( ( ) => onClose ( 'close-button' ) , [ onClose ] )
307297
308298 const header = ( renderHeader ?? DefaultHeader ) ( defaultedProps )
309299 const body = ( renderBody ?? DefaultBody ) ( defaultedProps )
310300 const footer = ( renderFooter ?? DefaultFooter ) ( defaultedProps )
311301
312302 return (
313- < >
314- < Portal >
315- < Backdrop ref = { backdropRef } >
316- < StyledDialog
317- width = { width }
318- height = { height }
319- ref = { dialogRef }
320- role = { role }
321- aria-labelledby = { dialogLabelId }
322- aria-describedby = { dialogDescriptionId }
323- aria-modal
324- sx = { sx }
325- >
326- { header }
327- { body }
328- { footer }
329- </ StyledDialog >
330- </ Backdrop >
331- </ Portal >
332- </ >
303+ < StyledDialog
304+ width = { width }
305+ height = { height === 'auto' ? 'min-content' : height }
306+ ref = { dialogRef }
307+ role = { role }
308+ aria-labelledby = { dialogLabelId }
309+ aria-describedby = { dialogDescriptionId }
310+ sx = { sx }
311+ onCancel = { onCancel }
312+ onClose = { onCloseHandler }
313+ >
314+ { header }
315+ { body }
316+ { footer }
317+ </ StyledDialog >
333318 )
334319} )
335320_Dialog . displayName = 'Dialog'
@@ -385,17 +370,10 @@ const buttonTypes = {
385370}
386371const Buttons : React . FC < React . PropsWithChildren < { buttons : DialogButtonProps [ ] } > > = ( { buttons} ) => {
387372 const autoFocusRef = useProvidedRefOrCreate < HTMLButtonElement > ( buttons . find ( button => button . autoFocus ) ?. ref )
388- let autoFocusCount = 0
389373 const [ hasRendered , setHasRendered ] = useState ( 0 )
390- useEffect ( ( ) => {
391- // hack to work around dialogs originating from other focus traps.
392- if ( hasRendered === 1 ) {
393- autoFocusRef . current ?. focus ( )
394- } else {
395- setHasRendered ( hasRendered + 1 )
396- }
374+ useIsomorphicLayoutEffect ( ( ) => {
375+ autoFocusRef . current ?. setAttribute ( 'autofocus' )
397376 } , [ autoFocusRef , hasRendered ] )
398-
399377 return (
400378 < >
401379 { buttons . map ( ( dialogButtonProps , index ) => {
0 commit comments