Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

♻️🚸Dialog: refactor to use native dialog #2950

Merged
merged 7 commits into from
Jul 7, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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])
}