Skip to content

Commit

Permalink
♻️🚸Dialog: refactor to use native dialog (#2950)
Browse files Browse the repository at this point in the history
* wip implement native dialog

* update dialog tests

* adding dialog ref, deprecate returnfocus

* Improve useHideBodyScroll

* some documentation fixes

* close only with onclose also with escape key

* change deprecation note
  • Loading branch information
oddvernes authored Jul 7, 2023
1 parent 219e7d6 commit 4d70773
Show file tree
Hide file tree
Showing 7 changed files with 117 additions and 64 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ const Demo = () => {
<Button aria-haspopup="dialog" onClick={handleOpen}>
Trigger Dialog
</Button>
<Dialog open={isOpen}>
<Dialog open={isOpen} isDismissable onClose={handleClose}>
<Dialog.Header>
<Dialog.Title>Title</Dialog.Title>
</Dialog.Header>
Expand Down
20 changes: 10 additions & 10 deletions packages/eds-core-react/src/components/Dialog/Dialog.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,16 @@ const RadioWrapper = styled(Radio)`
display: flex;
`

const Placeholder = styled.div`
background: rgba(255, 146, 0, 0.15);
border: 1px dashed #ff9200;
box-sizing: border-box;
border-radius: 4px;
padding: 8px;
width: 100%;
display: inline-block;
`

export const Introduction: StoryFn<DialogProps> = (args) => {
const { open, isDismissable } = args
const [, updateArgs] = useArgs()
Expand Down Expand Up @@ -165,16 +175,6 @@ export const PlaceholderPlusAction: StoryFn<DialogProps> = () => {
setIsOpen(false)
}

const Placeholder = styled.div`
background: rgba(255, 146, 0, 0.15);
border: 1px dashed #ff9200;
box-sizing: border-box;
border-radius: 4px;
padding: 8px;
width: 100%;
display: inline-block;
`

return (
<>
<Button aria-haspopup="dialog" onClick={handleOpen}>
Expand Down
12 changes: 8 additions & 4 deletions packages/eds-core-react/src/components/Dialog/Dialog.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@ const StyledDialog = styled(Dialog)`
min-height: ${minHeight};
width: ${width};
`
//dialog is not yet implemented in jsdom, keep an eye on: https://github.com/jsdom/jsdom/issues/3294
beforeAll(() => {
HTMLDialogElement.prototype.showModal = jest.fn()
HTMLDialogElement.prototype.close = jest.fn()
})

afterEach(cleanup)

Expand Down Expand Up @@ -60,10 +65,9 @@ describe('Dialog', () => {
it('Is dismissable with button click', () => {
render(<DismissableDialog data-testid="dialog" />)
const dialog = screen.getByTestId('dialog')

expect(dialog).toBeInTheDocument()
expect(screen.queryByText('OK')).toBeVisible()
const targetButton = screen.queryByText('OK')
expect(screen.getByText('OK')).toBeInTheDocument()
const targetButton = screen.getByText('OK')
fireEvent.click(targetButton)
expect(dialog).not.toBeInTheDocument()
})
Expand All @@ -72,7 +76,7 @@ describe('Dialog', () => {
const dialog = screen.getByTestId('dialog')

expect(dialog).toBeInTheDocument()
expect(screen.queryByText('OK')).toBeVisible()
expect(screen.getByText('OK')).toBeInTheDocument()
fireEvent.keyDown(dialog, {
key: 'Escape',
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ const {
colors: {
ui: {
background__default: { rgba: background },
background__scrim: { rgba: backdrop },
},
},
shape: {
Expand Down Expand Up @@ -53,6 +54,9 @@ export const dialog: DialogToken = {
actions: {
minHeight: '48px',
},
scrim: {
background: backdrop,
},
},
modes: {
compact: {},
Expand Down
103 changes: 65 additions & 38 deletions packages/eds-core-react/src/components/Dialog/Dialog.tsx
Original file line number Diff line number Diff line change
@@ -1,27 +1,27 @@
import { forwardRef, useMemo } from 'react'
import {
forwardRef,
useEffect,
useRef,
MouseEvent,
ForwardedRef,
useMemo,
} from 'react'
import styled, { css, ThemeProvider } from 'styled-components'
import {
typographyTemplate,
bordersTemplate,
useToken,
useGlobalKeyPress,
useHideBodyScroll,
mergeRefs,
} from '@equinor/eds-utils'
import { Paper } from '../Paper'
import { Scrim } from '../Scrim'
import { dialog as dialogToken } from './Dialog.tokens'
import { useEds } from '../EdsProvider'
import {
FloatingPortal,
useFloating,
FloatingFocusManager,
} from '@floating-ui/react'

const StyledDialog = styled(Paper).attrs<DialogProps>({
tabIndex: 0,
role: 'dialog',
'aria-labelledby': 'eds-dialog-title',
'aria-describedby': 'eds-dialog-customcontent',
'aria-modal': true,
})(({ theme }) => {
return css`
width: ${theme.width};
Expand All @@ -35,16 +35,33 @@ const StyledDialog = styled(Paper).attrs<DialogProps>({
`
})

const StyledNativeDialog = styled.dialog(({ theme }) => {
return css`
padding: 0;
border: 0;
overflow: visible;
overscroll-behavior: contain;
${bordersTemplate(theme.border)};
&::backdrop {
background-color: ${theme.entities.scrim.background};
}
`
})

export type DialogProps = {
/** Whether Dialog can be dismissed with esc key and outside click
*/
isDismissable?: boolean
/** programmatically toggle dialog */
open: boolean
/** callback to handle closing scrim */
/** callback to handle closing */
onClose?: () => void
/** Wheter the dialog should return focus to the previous focused element */
/**
* return focus to the previous focused element
* @deprecated Component is now based on native dialog where focus is handled by the browser automatically
* */
returnFocus?: boolean
dialogRef?: ForwardedRef<HTMLDialogElement>
} & React.HTMLAttributes<HTMLDivElement>

export const Dialog = forwardRef<HTMLDivElement, DialogProps>(function Dialog(
Expand All @@ -53,46 +70,56 @@ export const Dialog = forwardRef<HTMLDivElement, DialogProps>(function Dialog(
open,
onClose,
isDismissable = false,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
returnFocus = true,
dialogRef,
...props
},
ref,
) {
const { density } = useEds()
const localRef = useRef<HTMLDialogElement>(null)
const token = useToken({ density }, dialogToken)
const { refs, context } = useFloating()
const handleDismiss = () => {
onClose && onClose()
}

const dialogRef = useMemo(
() => mergeRefs<HTMLDivElement>(refs.setFloating, ref),
[refs.setFloating, ref],
const combinedDialogRef = useMemo(
() => mergeRefs<HTMLDialogElement>(localRef, dialogRef),
[localRef, dialogRef],
)

const rest = {
...props,
open,
useEffect(() => {
if (open && !localRef?.current?.hasAttribute('open')) {
localRef?.current?.showModal()
} else {
localRef?.current?.close()
}
}, [open])

//This might become redundant in the future, see https://github.com/whatwg/html/issues/7732
useHideBodyScroll(open)

const handleDismiss = (e: MouseEvent) => {
const { target } = e
if (target instanceof HTMLElement)
if (isDismissable && target.nodeName === 'DIALOG') {
onClose && onClose()
}
}
useGlobalKeyPress('Escape', (e) => {
e.preventDefault()
if (isDismissable && onClose && open) {
onClose && onClose()
}
})

return (
<FloatingPortal id="eds-dialog-container">
<ThemeProvider theme={token}>
<ThemeProvider theme={token}>
<StyledNativeDialog ref={combinedDialogRef} onClick={handleDismiss}>
{open && (
<Scrim open isDismissable={isDismissable} onClose={handleDismiss}>
<FloatingFocusManager
context={context}
modal
returnFocus={returnFocus}
>
<StyledDialog elevation="above_scrim" {...rest} ref={dialogRef}>
{children}
</StyledDialog>
</FloatingFocusManager>
</Scrim>
<StyledDialog elevation="above_scrim" {...props} ref={ref}>
{children}
</StyledDialog>
)}
</ThemeProvider>
</FloatingPortal>
</StyledNativeDialog>
</ThemeProvider>
)
})

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,12 +83,8 @@ exports[`Dialog Matches snapshot 1`] = `
<div
aria-describedby="eds-dialog-customcontent"
aria-labelledby="eds-dialog-title"
aria-modal="true"
class="c0 c1"
data-testid="dialog"
open=""
role="dialog"
tabindex="-1"
>
<div
class="c2"
Expand Down
36 changes: 29 additions & 7 deletions packages/eds-utils/src/hooks/useHideBodyScroll.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,39 @@
import { useEffect, useRef } from 'react'

type Styles = {
overflow: string
paddingRight: string
}

export const useHideBodyScroll = (active: boolean): void => {
const overflowState = useRef<string | undefined>()
const originalStyles = useRef<Styles | undefined>()
useEffect(() => {
if (typeof document === 'undefined') return
const html = document.documentElement
const { body } = document

if (active) {
overflowState.current = document.body.style.overflow
document.body.style.overflow = 'hidden'
} else {
document.body.style.overflow = overflowState.current
const scrollBarWidth = window.innerWidth - html.clientWidth
const bodyPaddingRight =
parseInt(
window.getComputedStyle(body).getPropertyValue('padding-right'),
) || 0
const oldStyle: Styles = {
overflow: body.style.overflow,
paddingRight: body.style.paddingRight,
}
originalStyles.current = oldStyle

body.style.overflow = 'hidden'
body.style.paddingRight = `${bodyPaddingRight + scrollBarWidth}px`
} else if (originalStyles.current) {
body.style.overflow = originalStyles.current.overflow
body.style.paddingRight = originalStyles.current.paddingRight
}
const originalState = overflowState.current
const originalState = originalStyles.current
return () => {
document.body.style.overflow = originalState
body.style.overflow = originalState?.overflow
body.style.paddingRight = originalState?.paddingRight
}
}, [active])
}

0 comments on commit 4d70773

Please sign in to comment.