958e3ea8c6
* apply re-focus bug fix to Popover * force focus in Menu.Items from within Menu.Items component itself * force focus in Listbox.Options from within Listbox.Options component itself * fix undefined values in id's We were setting the element in state, but updates to the id were not taken into account * update the caniuse db * ensure useInertOthers works in multiple places Previously each hook call would take care of the whole tree. However when multiple calls to this hook are happening we need to make sure that you are not removing the aria-hidden when another hook is still used. This will fix that by keeping track of a list of "interactable" items, and updating the parents (root of the body) accordingly. * add the concept of a Stack When you are rendering a Dialog, we will make sure that this Dialog is rendered inside a Portal. However, when you are also rendering a Menu, there is a chance that your Menu doesn't fit within the Dialog, therefore you will likely render the Menu.Items inside a Portal so that you can style it as if it is rendered inside but overflows the Dialog correctly. This introduces an interesting/annoying problem. Your Menu.Items are now rendered in a Portal, as a *sibling* to the Dialog. This means that autoFocus, focusTrap, ... all these features don't work as expected. Introducing this Stack will allow us to register DOM nodes into a list of contains that we consider being part of the main container. In other words, the sibling Menu.Items will now be considered part of the Dialog. Even though it is rendered *outside* of the Dialog. This concept also allows for some fun stuff, for example, nesting Dialog's is no problem with this approach. Dialogs are technically rendered as siblings in the Portal, but the FocusTrap, and all that just works as expected. * capture keyboard events in the capturing phase This will allow us to use event.stopPropagation() in the code (which will be required, probably) but still see the keystrokes in the playground. * stop propagating keyboard events This looks a bit silly, and ideally we can solve this in a more elegant way. However when you nest a Menu inside a Dialog, both of those components have a `close on escape` functionality built in. However when your Menu is open, and you press escape, you only want to close the Menu, not the Dialog. Therefore if we `event.stopPropagation()` it allows us to stop the `escape` keystroke in the Menu from reaching all the way to the Dialog itself. * update Dialog example that showcases nested Dialogs, and nested Menu * update changelog
260 lines
13 KiB
TypeScript
260 lines
13 KiB
TypeScript
import React, { useState, Fragment } from 'react'
|
|
import { Dialog, Menu, Portal, Transition } from '@headlessui/react'
|
|
import { usePopper } from '../../playground-utils/hooks/use-popper'
|
|
|
|
function classNames(...classes) {
|
|
return classes.filter(Boolean).join(' ')
|
|
}
|
|
|
|
function resolveClass({ active, disabled }) {
|
|
return classNames(
|
|
'flex justify-between w-full px-4 py-2 text-sm leading-5 text-left',
|
|
active ? 'bg-gray-100 text-gray-900' : 'text-gray-700',
|
|
disabled && 'cursor-not-allowed opacity-50'
|
|
)
|
|
}
|
|
|
|
function Nested({ onClose, level = 0 }) {
|
|
let [showChild, setShowChild] = useState(false)
|
|
|
|
return (
|
|
<>
|
|
<Dialog open={true} onClose={onClose} className="fixed z-10 inset-0 pointer-events-none">
|
|
{true && <Dialog.Overlay className="fixed inset-0 bg-gray-500 opacity-25" />}
|
|
<div
|
|
className="z-10 fixed left-12 top-24 bg-white w-96 p-4 pointer-events-auto"
|
|
style={{
|
|
transform: `translate(calc(50px * ${level}), calc(50px * ${level}))`,
|
|
}}
|
|
>
|
|
<p>Level: {level}</p>
|
|
<div className="space-x-4">
|
|
<button className="bg-gray-200 px-2 py-1 rounded" onClick={() => setShowChild(true)}>
|
|
Open (1)
|
|
</button>
|
|
<button className="bg-gray-200 px-2 py-1 rounded" onClick={() => setShowChild(true)}>
|
|
Open (2)
|
|
</button>
|
|
<button className="bg-gray-200 px-2 py-1 rounded" onClick={() => setShowChild(true)}>
|
|
Open (3)
|
|
</button>
|
|
</div>
|
|
</div>
|
|
{showChild && <Nested onClose={() => setShowChild(false)} level={level + 1} />}
|
|
</Dialog>
|
|
</>
|
|
)
|
|
}
|
|
|
|
export default function Home() {
|
|
let [isOpen, setIsOpen] = useState(false)
|
|
let [nested, setNested] = useState(false)
|
|
|
|
let [trigger, container] = usePopper({
|
|
placement: 'bottom-end',
|
|
strategy: 'fixed',
|
|
modifiers: [{ name: 'offset', options: { offset: [0, 10] } }],
|
|
})
|
|
|
|
return (
|
|
<>
|
|
<button
|
|
type="button"
|
|
onClick={() => setIsOpen(v => !v)}
|
|
className="m-12 px-4 py-2 text-base font-medium leading-6 text-gray-700 transition duration-150 ease-in-out bg-white border border-gray-300 rounded-md shadow-sm hover:text-gray-500 focus:outline-none focus:border-blue-300 focus:shadow-outline-blue sm:text-sm sm:leading-5"
|
|
>
|
|
Toggle!
|
|
</button>
|
|
|
|
<button onClick={() => setNested(true)}>Show nested</button>
|
|
{nested && <Nested onClose={() => setNested(false)} />}
|
|
|
|
<Transition show={isOpen} as={Fragment}>
|
|
<Dialog open={isOpen} onClose={setIsOpen} static>
|
|
<div className="fixed z-10 inset-0 overflow-y-auto">
|
|
<div className="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
|
|
<Transition.Child
|
|
enter="ease-out duration-300"
|
|
enterFrom="opacity-0"
|
|
enterTo="opacity-100"
|
|
leave="ease-in duration-200"
|
|
leaveFrom="opacity-100"
|
|
leaveTo="opacity-0"
|
|
>
|
|
<Dialog.Overlay className="fixed inset-0 transition-opacity">
|
|
<div className="absolute inset-0 bg-gray-500 opacity-75"></div>
|
|
</Dialog.Overlay>
|
|
</Transition.Child>
|
|
|
|
<Transition.Child
|
|
enter="ease-out transform 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 transform duration-200"
|
|
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
|
|
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
|
>
|
|
{/* This element is to trick the browser into centering the modal contents. */}
|
|
<span
|
|
className="hidden sm:inline-block sm:align-middle sm:h-screen"
|
|
aria-hidden="true"
|
|
>
|
|
​
|
|
</span>
|
|
<div className="inline-block align-bottom bg-white rounded-lg text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-lg sm:w-full">
|
|
<div className="bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
|
|
<div className="sm:flex sm:items-start">
|
|
<div className="mx-auto flex-shrink-0 flex items-center justify-center h-12 w-12 rounded-full bg-red-100 sm:mx-0 sm:h-10 sm:w-10">
|
|
{/* Heroicon name: exclamation */}
|
|
<svg
|
|
className="h-6 w-6 text-red-600"
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
fill="none"
|
|
viewBox="0 0 24 24"
|
|
stroke="currentColor"
|
|
aria-hidden="true"
|
|
>
|
|
<path
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
strokeWidth={2}
|
|
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"
|
|
/>
|
|
</svg>
|
|
</div>
|
|
<div className="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
|
|
<Dialog.Title
|
|
as="h3"
|
|
className="text-lg leading-6 font-medium 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. This action cannot be undone.
|
|
</p>
|
|
<div className="relative inline-block text-left mt-10">
|
|
<Menu>
|
|
{({ open }) => (
|
|
<>
|
|
<span className="rounded-md shadow-sm">
|
|
<Menu.Button
|
|
ref={trigger}
|
|
className="inline-flex justify-center w-full px-4 py-2 text-sm font-medium leading-5 text-gray-700 transition duration-150 ease-in-out bg-white border border-gray-300 rounded-md hover:text-gray-500 focus:outline-none focus:border-blue-300 focus:shadow-outline-blue active:bg-gray-50 active:text-gray-800"
|
|
>
|
|
<span>Choose a reason</span>
|
|
<svg
|
|
className="w-5 h-5 ml-2 -mr-1"
|
|
viewBox="0 0 20 20"
|
|
fill="currentColor"
|
|
>
|
|
<path
|
|
fillRule="evenodd"
|
|
d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z"
|
|
clipRule="evenodd"
|
|
/>
|
|
</svg>
|
|
</Menu.Button>
|
|
</span>
|
|
|
|
<Transition
|
|
show={open}
|
|
enter="transition duration-100 ease-out"
|
|
enterFrom="transform scale-95 opacity-0"
|
|
enterTo="transform scale-100 opacity-100"
|
|
leave="transition duration-75 ease-out"
|
|
leaveFrom="transform scale-100 opacity-100"
|
|
leaveTo="transform scale-95 opacity-0"
|
|
>
|
|
<Portal>
|
|
<Menu.Items
|
|
ref={container}
|
|
static
|
|
className="z-20 w-56 mt-2 origin-top-right bg-white border border-gray-200 divide-y divide-gray-100 rounded-md shadow-lg outline-none"
|
|
>
|
|
<div className="px-4 py-3">
|
|
<p className="text-sm leading-5">Signed in as</p>
|
|
<p className="text-sm font-medium leading-5 text-gray-900 truncate">
|
|
tom@example.com
|
|
</p>
|
|
</div>
|
|
|
|
<div className="py-1">
|
|
<Menu.Item
|
|
as="a"
|
|
href="#account-settings"
|
|
className={resolveClass}
|
|
>
|
|
Account settings
|
|
</Menu.Item>
|
|
<Menu.Item
|
|
as="a"
|
|
href="#support"
|
|
className={resolveClass}
|
|
>
|
|
Support
|
|
</Menu.Item>
|
|
<Menu.Item
|
|
as="a"
|
|
disabled
|
|
href="#new-feature"
|
|
className={resolveClass}
|
|
>
|
|
New feature (soon)
|
|
</Menu.Item>
|
|
<Menu.Item
|
|
as="a"
|
|
href="#license"
|
|
className={resolveClass}
|
|
>
|
|
License
|
|
</Menu.Item>
|
|
</div>
|
|
|
|
<div className="py-1">
|
|
<Menu.Item
|
|
as="a"
|
|
href="#sign-out"
|
|
className={resolveClass}
|
|
>
|
|
Sign out
|
|
</Menu.Item>
|
|
</div>
|
|
</Menu.Items>
|
|
</Portal>
|
|
</Transition>
|
|
</>
|
|
)}
|
|
</Menu>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div className="bg-gray-50 px-4 py-3 sm:px-6 sm:flex sm:flex-row-reverse">
|
|
<button
|
|
type="button"
|
|
onClick={() => setIsOpen(false)}
|
|
className="w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-red-600 text-base font-medium text-white hover:bg-red-700 focus:outline-none focus:shadow-outline-red sm:ml-3 sm:w-auto sm:text-sm"
|
|
>
|
|
Deactivate
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onClick={() => setIsOpen(false)}
|
|
className="mt-3 w-full inline-flex justify-center rounded-md border border-gray-300 shadow-sm px-4 py-2 bg-white text-base font-medium text-gray-700 hover:text-gray-500 focus:outline-none focus:shadow-outline-indigo sm:mt-0 sm:w-auto sm:text-sm"
|
|
>
|
|
Cancel
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</Transition.Child>
|
|
</div>
|
|
</div>
|
|
</Dialog>
|
|
</Transition>
|
|
</>
|
|
)
|
|
}
|