Skip to content

Commit

Permalink
WIP
Browse files Browse the repository at this point in the history
  • Loading branch information
RobinMalfait committed Jun 2, 2022
1 parent 4ccbfae commit d531051
Show file tree
Hide file tree
Showing 5 changed files with 129 additions and 56 deletions.
10 changes: 6 additions & 4 deletions packages/@headlessui-react/src/components/dialog/dialog.test.tsx
Expand Up @@ -781,7 +781,7 @@ describe('Mouse interactions', () => {
})
)

it(
fit(
'should be possible to close the dialog, and keep focus on the focusable element',
suppressConsoleLogs(async () => {
function Example() {
Expand Down Expand Up @@ -889,9 +889,11 @@ describe('Mouse interactions', () => {
return (
<div onClick={wrapperFn}>
<Dialog open={isOpen} onClose={setIsOpen}>
Contents
<button onClick={() => setIsOpen(false)}>Inside</button>
<TabSentinel />
<Dialog.Panel>
Contents
<button onClick={() => setIsOpen(false)}>Inside</button>
<TabSentinel />
</Dialog.Panel>
</Dialog>
</div>
)
Expand Down
22 changes: 10 additions & 12 deletions packages/@headlessui-react/src/components/dialog/dialog.tsx
Expand Up @@ -32,7 +32,7 @@ import { Description, useDescriptions } from '../description/description'
import { useOpenClosed, State } from '../../internal/open-closed'
import { useServerHandoffComplete } from '../../hooks/use-server-handoff-complete'
import { StackProvider, StackMessage } from '../../internal/stack-context'
import { useOutsideClick, Features as OutsideClickFeatures } from '../../hooks/use-outside-click'
import { useOutsideClick } from '../../hooks/use-outside-click'
import { getOwnerDocument } from '../../utils/owner'
import { useOwnerDocument } from '../../hooks/use-owner'
import { useEventListener } from '../../hooks/use-event-listener'
Expand Down Expand Up @@ -100,13 +100,7 @@ let DEFAULT_DIALOG_TAG = 'div' as const
interface DialogRenderPropArg {
open: boolean
}
type DialogPropsWeControl =
| 'id'
| 'role'
| 'aria-modal'
| 'aria-describedby'
| 'aria-labelledby'
| 'onClick'
type DialogPropsWeControl = 'id' | 'role' | 'aria-modal' | 'aria-describedby' | 'aria-labelledby'

let DialogRenderFeatures = Features.RenderStrategy | Features.Static

Expand Down Expand Up @@ -224,7 +218,7 @@ let DialogRoot = forwardRefWithAs(function Dialog<

close()
},
OutsideClickFeatures.IgnoreScrollbars
enabled
)

// Handle `Escape` to close
Expand Down Expand Up @@ -311,9 +305,6 @@ let DialogRoot = forwardRefWithAs(function Dialog<
'aria-modal': dialogState === DialogStates.Open ? true : undefined,
'aria-labelledby': state.titleId,
'aria-describedby': describedby,
onClick(event: ReactMouseEvent) {
event.stopPropagation()
},
}

return (
Expand Down Expand Up @@ -492,10 +483,17 @@ let Panel = forwardRefWithAs(function Panel<TTag extends ElementType = typeof DE
[dialogState]
)

// Prevent the click events inside the Dialog.Panel from bubbling through the React Tree which
// could submit wrapping <form> elements even if we portalled the Dialog.
let handleClick = useEvent((event: ReactMouseEvent) => {
event.stopPropagation()
})

let theirProps = props
let ourProps = {
ref: panelRef,
id,
onClick: handleClick,
}

return render({
Expand Down
48 changes: 11 additions & 37 deletions packages/@headlessui-react/src/hooks/use-outside-click.ts
@@ -1,29 +1,25 @@
import { MutableRefObject, useRef } from 'react'
import { microTask } from '../utils/micro-task'
import { useEvent } from './use-event'
import { MutableRefObject, useEffect, useRef } from 'react'
import { useWindowEvent } from './use-window-event'

type Container = MutableRefObject<HTMLElement | null> | HTMLElement | null
type ContainerCollection = Container[] | Set<Container>
type ContainerInput = Container | ContainerCollection

export enum Features {
None = 1 << 0,
IgnoreScrollbars = 1 << 1,
}

export function useOutsideClick(
containers: ContainerInput | (() => ContainerInput),
cb: (event: MouseEvent | PointerEvent, target: HTMLElement) => void,
features: Features = Features.None
enabled: boolean = true
) {
let called = useRef(false)
let handler = useEvent((event: MouseEvent | PointerEvent) => {
if (called.current) return
called.current = true
microTask(() => {
called.current = false
// TODO: remove this once the React bug has been fixed: https://github.com/facebook/react/issues/24657
let enabledRef = useRef(false)
useEffect(() => {
requestAnimationFrame(() => {
enabledRef.current = enabled
})
}, [enabled])

useWindowEvent('click', (event) => {
if (!enabledRef.current) return

let _containers = (function resolve(containers): ContainerCollection {
if (typeof containers === 'function') {
Expand All @@ -46,25 +42,6 @@ export function useOutsideClick(
// Ignore if the target doesn't exist in the DOM anymore
if (!target.ownerDocument.documentElement.contains(target)) return

// Ignore scrollbars:
// This is a bit hacky, and is only necessary because we are checking for `pointerdown` and
// `mousedown` events. They _are_ being called if you click on a scrollbar. The `click` event
// is not called when clicking on a scrollbar, but we can't use that otherwise it won't work
// on mobile devices where only pointer events are being used.
if ((features & Features.IgnoreScrollbars) === Features.IgnoreScrollbars) {
// TODO: We can calculate this dynamically~is. On macOS if you have the "Automatically based
// on mouse or trackpad" setting enabled, then the scrollbar will float on top and therefore
// you can't calculate its with by checking the clientWidth and scrollWidth of the element.
// Therefore we are currently hardcoding this to be 20px.
let scrollbarWidth = 20

let viewport = target.ownerDocument.documentElement
if (event.clientX > viewport.clientWidth - scrollbarWidth) return
if (event.clientX < scrollbarWidth) return
if (event.clientY > viewport.clientHeight - scrollbarWidth) return
if (event.clientY < scrollbarWidth) return
}

// Ignore if the target exists in one of the containers
for (let container of _containers) {
if (container === null) continue
Expand All @@ -76,7 +53,4 @@ export function useOutsideClick(

return cb(event, target)
})

useWindowEvent('pointerdown', handler)
useWindowEvent('mousedown', handler)
}
Expand Up @@ -3,13 +3,14 @@ import { Dialog, Tab } from '@headlessui/react'

export default function App() {
let [open, setOpen] = useState(false)
console.log({ open })

return (
<>
<button onClick={() => setOpen(true)}>Open dialog</button>
<Dialog open={open} onClose={setOpen} className="fixed inset-0 grid place-content-center">
<Dialog.Overlay className="fixed inset-0 bg-gray-500/70" />
<div className="inline-block transform overflow-hidden rounded-lg bg-white text-left align-bottom shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg sm:align-middle">
<div className="fixed inset-0 bg-gray-500/70" />
<Dialog.Panel className="inline-block transform overflow-hidden rounded-lg bg-white text-left align-bottom shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg sm:align-middle">
<div className="bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
<Tab.Group>
<Tab.List>
Expand All @@ -24,7 +25,7 @@ export default function App() {
</Tab.Panels>
</Tab.Group>
</div>
</div>
</Dialog.Panel>
</Dialog>
</>
)
Expand Down
98 changes: 98 additions & 0 deletions packages/playground-react/pages/dialog/scrollable-dialog.tsx
@@ -0,0 +1,98 @@
import { Fragment, useRef, useState } from 'react'
import { Dialog, Transition } from '@headlessui/react'
import { ExclamationIcon } from '@heroicons/react/outline'

export default function Example() {
const [open, setOpen] = useState(false)

const cancelButtonRef = useRef(null)

return (
<>
<div>
<button
type="button"
className="inline-flex items-center rounded-md border border-gray-300 bg-white px-6 py-3 text-base font-medium text-gray-700 shadow-sm hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"
onClick={() => setOpen(true)}
>
Open Dialog
</button>
</div>
<Transition.Root show={open} as={Fragment}>
<Dialog as="div" className="relative z-10" initialFocus={cancelButtonRef} onClose={setOpen}>
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-in duration-200"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" />
</Transition.Child>

<div className="fixed inset-0 z-10 overflow-y-auto">
<div className="flex min-h-full items-end justify-center p-4 text-center sm:items-center sm:p-0">
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
enterTo="opacity-100 translate-y-0 sm:scale-100"
leave="ease-in duration-200"
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
>
<Dialog.Panel className="relative transform overflow-hidden rounded-lg bg-white px-4 pt-5 pb-4 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg sm:p-6">
<div className="sm:flex sm:items-start">
<div className="mx-auto flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-full bg-red-100 sm:mx-0 sm:h-10 sm:w-10">
<ExclamationIcon className="h-6 w-6 text-red-600" aria-hidden="true" />
</div>
<div className="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
<Dialog.Title as="h3" className="text-lg font-medium leading-6 text-gray-900">
Deactivate account
</Dialog.Title>
<div className="mt-2">
<p className="text-sm text-gray-500">
Are you sure you want to deactivate your account? All of your data will be
permanently removed from our servers forever. This action cannot be
undone.
</p>
{Array(20)
.fill(null)
.map((_, i) => (
<p key={i} className="text-sm text-gray-500">
Are you sure you want to deactivate your account? All of your data
will be permanently removed from our servers forever. This action
cannot be undone.
</p>
))}
</div>
</div>
</div>
<div className="mt-5 sm:mt-4 sm:ml-10 sm:flex sm:pl-4">
<button
type="button"
className="inline-flex w-full justify-center rounded-md border border-transparent bg-red-600 px-4 py-2 text-base font-medium text-white shadow-sm hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-2 sm:w-auto sm:text-sm"
onClick={() => setOpen(false)}
>
Deactivate
</button>
<button
type="button"
className="mt-3 inline-flex w-full justify-center rounded-md border border-gray-300 bg-white px-4 py-2 text-base font-medium text-gray-700 shadow-sm hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 sm:mt-0 sm:ml-3 sm:w-auto sm:text-sm"
onClick={() => setOpen(false)}
ref={cancelButtonRef}
>
Cancel
</button>
</div>
</Dialog.Panel>
</Transition.Child>
</div>
</div>
</Dialog>
</Transition.Root>
</>
)
}

0 comments on commit d531051

Please sign in to comment.