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'
14- import { useRefObjectAsForwardedRef } from '../hooks/useRefObjectAsForwardedRef'
11+ import useIsomorphicLayoutEffect from '../utils/useIsomorphicLayoutEffect'
1512import { useId } from '../hooks/useId'
13+ import { useProvidedRefOrCreate } from '../hooks/useProvidedRefOrCreate'
14+ import { useRefObjectAsForwardedRef } from '../hooks/useRefObjectAsForwardedRef'
1615
1716/* Dialog Version 2 */
1817
@@ -95,13 +94,23 @@ export interface DialogProps extends SxProp {
9594 footerButtons ?: DialogButtonProps [ ]
9695
9796 /**
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').
97+ * This method is invoked when the dialog has been closed. This could
98+ * be from the Dialog.
99+ * @param gesture - Deprecated: The gesture argument was used to
100+ * indicate if the gesture was from the close-button or escape button.
101+ * It will always be `'close-button'`, to check for _cancelations_ of
102+ * the dialog using the Esc key, use onCancel.
102103 */
103104 onClose : ( gesture : 'close-button' | 'escape' ) => void
104105
106+ /**
107+ * This method is invoked when the user instructs the browser they wish to
108+ * dismiss the dialog. Typically this means they have pressed the `Esc` key.
109+ *
110+ * @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLDialogElement/cancel_event
111+ */
112+ onCancel ?: ( ) => void
113+
105114 /**
106115 * Default: "dialog". The ARIA role to assign to this dialog.
107116 * @see https://www.w3.org/TR/wai-aria-practices-1.1/#dialog_modal
@@ -125,6 +134,11 @@ export interface DialogProps extends SxProp {
125134 * auto: variable based on contents
126135 */
127136 height ?: DialogHeight
137+
138+ /**
139+ * Whether or not the Dialog is rendered initially open
140+ */
141+ initiallyOpen ?: boolean
128142}
129143
130144/**
@@ -144,32 +158,11 @@ export interface DialogHeaderProps extends DialogProps {
144158 dialogDescriptionId : string
145159}
146160
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-
169161const heightMap = {
170162 small : '480px' ,
171163 large : '640px' ,
172164 auto : 'auto' ,
165+ 'min-content' : 'min-content' ,
173166} as const
174167
175168const widthMap = {
@@ -187,19 +180,40 @@ type StyledDialogProps = {
187180 height ?: DialogHeight
188181} & SxProp
189182
190- const StyledDialog = styled . div < StyledDialogProps > `
191- display: flex;
183+ const StyledDialog = styled . dialog < StyledDialogProps > `
192184 flex-direction: column;
193185 background-color: ${ get ( 'colors.canvas.overlay' ) } ;
186+ color: ${ get ( 'colors.fg.default' ) } ;
194187 box-shadow: ${ get ( 'shadows.overlay.shadow' ) } ;
195188 min-width: 296px;
196189 max-width: calc(100vw - 64px);
197190 max-height: calc(100vh - 64px);
198191 width: ${ props => widthMap [ props . width ?? ( 'xlarge' as const ) ] } ;
199192 height: ${ props => heightMap [ props . height ?? ( 'auto' as const ) ] } ;
193+ border: 0;
200194 border-radius: 12px;
195+ padding: 0;
201196 opacity: 1;
202197 animation: overlay--dialog-appear ${ ANIMATION_DURATION } ${ get ( 'animation.easeOutCubic' ) } ;
198+ overflow: initial;
199+
200+ &[open] {
201+ display: flex;
202+ }
203+
204+ &::backdrop {
205+ background-color: ${ get ( 'colors.primer.canvas.backdrop' ) } ;
206+ animation: dialog-backdrop-appear ${ ANIMATION_DURATION } ${ get ( 'animation.easeOutCubic' ) } ;
207+ }
208+
209+ @keyframes dialog-backdrop-appear {
210+ 0% {
211+ opacity: 0;
212+ }
213+ 100% {
214+ opacity: 1;
215+ }
216+ }
203217
204218 @keyframes overlay--dialog-appear {
205219 0% {
@@ -252,84 +266,56 @@ const DefaultFooter: React.FC<React.PropsWithChildren<DialogProps>> = ({footerBu
252266 ) : null
253267}
254268
255- const _Dialog = React . forwardRef < HTMLDivElement , React . PropsWithChildren < DialogProps > > ( ( props , forwardedRef ) => {
269+ const _Dialog = React . forwardRef < HTMLDialogElement , React . PropsWithChildren < DialogProps > > ( ( props , forwardedRef ) => {
256270 const {
257271 title = 'Dialog' ,
258272 subtitle = '' ,
259273 renderHeader,
260274 renderBody,
261275 renderFooter,
262276 onClose,
277+ onCancel,
263278 role = 'dialog' ,
264279 width = 'xlarge' ,
265280 height = 'auto' ,
266- footerButtons = [ ] ,
281+ initiallyOpen = true ,
267282 sx,
268283 } = props
269284 const dialogLabelId = useId ( )
270285 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- }
277286 const defaultedProps = { ...props , title, subtitle, role, dialogLabelId, dialogDescriptionId}
278287
279- const dialogRef = useRef < HTMLDivElement > ( null )
288+ const dialogRef = useRef < HTMLDialogElement > ( null )
280289 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- )
291290
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
291+ useIsomorphicLayoutEffect ( ( ) => {
292+ if ( initiallyOpen && ! dialogRef . current ?. open && dialogRef . current ?. isConnected ) {
293+ dialogRef . current . showModal ( )
299294 }
295+ } , [ initiallyOpen , dialogRef ] )
300296
301- document . body . style . overflow = 'hidden'
302-
303- return ( ) => {
304- document . body . style . overflow = bodyOverflowStyle
305- }
306- } , [ ] )
297+ const onCloseHandler = useCallback ( ( ) => onClose ( 'close-button' ) , [ onClose ] )
307298
308299 const header = ( renderHeader ?? DefaultHeader ) ( defaultedProps )
309300 const body = ( renderBody ?? DefaultBody ) ( defaultedProps )
310301 const footer = ( renderFooter ?? DefaultFooter ) ( defaultedProps )
311302
312303 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- </ >
304+ < StyledDialog
305+ width = { width }
306+ height = { height === 'auto' ? 'min-content' : height }
307+ ref = { dialogRef }
308+ role = { role }
309+ aria-labelledby = { dialogLabelId }
310+ aria-describedby = { dialogDescriptionId }
311+ sx = { sx }
312+ onCancel = { onCancel }
313+ onClose = { onCloseHandler }
314+ >
315+ { header }
316+ { body }
317+ { footer }
318+ </ StyledDialog >
333319 )
334320} )
335321_Dialog . displayName = 'Dialog'
@@ -386,16 +372,9 @@ const buttonTypes = {
386372const Buttons : React . FC < React . PropsWithChildren < { buttons : DialogButtonProps [ ] } > > = ( { buttons} ) => {
387373 const autoFocusRef = useProvidedRefOrCreate < HTMLButtonElement > ( buttons . find ( button => button . autoFocus ) ?. ref )
388374 let autoFocusCount = 0
389- 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- }
397- } , [ autoFocusRef , hasRendered ] )
398-
375+ useIsomorphicLayoutEffect ( ( ) => {
376+ autoFocusRef . current ?. setAttribute ( 'autofocus' , '' )
377+ } , [ autoFocusRef ] )
399378 return (
400379 < >
401380 { buttons . map ( ( dialogButtonProps , index ) => {
0 commit comments