Skip to content

Commit

Permalink
Multiple new components (#220)
Browse files Browse the repository at this point in the history
* add Disclosure component

* expose the Disclosure component

* add Disclosure example component page

* temporary fix selector because of JSDOM bug

* add useFocusTrap hook

* add FocusTrap component

* expose FocusTrap

* add Dialog component

* add Dialog example component page

* expose Dialog

* random cleanup

* make TypeScript a bit more happy

* add Switch.Description component for React

* add Switch.Description component for Vue

* ensure focus event is triggered on click when element is focusable

* remove Dialog.Button and Dialog.Panel from accessibility assertions

* add Portal component

* expose Portal

* always render Dialog in a Portal

* add useInertOthers hook

This will allow us to mark everything but the current ref as "inert".
This is important for screenreaders, to ensure that screenreaders and
assistive technology can't interact with other content but the current
ref.

This implementation is not ideal yet. It doesn't take into account that
you can use the hook in 2 different components. For now this is fine,
since we only use it in a Dialog and you should also probably only have
a single Dialog open at a time.

Will improve this in the future!

* use the useInertOthers hook

* add scroll lock to the dialog

* ensure we respect autoFocus on form elements within the Dialog

If we have an autoFocus on an input, that input will receive focus. Once
we try to focus the first focusable element in the Dialog this could be
lead to unwanted behaviour. Therefore we check if the focus already is
within the Dialog, if it is, keep it like that.

* only mark aria-modal when Dialog is open

* add initialFocus option to Dialog, FocusTrap & useFocusTrap

* add tests and a few fixes for the initialFocusRef functionality

* forward ref to underlying Dialog component

* close Dialog when it becomes hidden

Could happen when this is in md:hidden for example

* prevent infinite loop

When we `Tab` in a FocusTrap it will try and focus the Next element. If
we are in a state where none of the elements inside the FocusTrap can be
focused, then we keep trying to focus the next one in line. This results
in an infinite loop...

To mitigate this issue, we check if we looped around, if we did, it
means that we tried all the other focusable elements, therefore we can
stop.

* isIntersecting doesn't work in every scenario

When page is scrollable, when dialog is translated of the page. Now just checking for sizes, which should be enough for md:hiden cases

* render Portal contents in a div

Otherwise you can't use multiple Portal components if you render multiple children inside each Portal

* ensure the props bag is typed

* add getByText and assertContainsActiveElement helpers

* add Popover component

* expose Popover

* add Popover example component page

* add quick checks to prevent useless renders

* drop incorrect close function

* update Changelog

* make test error more readable when comparing DOM nodes

* actually call .focus() on the element

This ensures that the document.activeElement becomes the focused element.

* improve useSyncRefs, because ...refs is *always* different

* add dedicated focus management utilities

* refactor useFocusTrap, use focus management utilities

* fix regression while using outside click

There might be a chance that you didn't even notice this *bug*. The idea
is that when you click outside, that the Menu or Listbox closes. However
there is another step that happens:

1. When you click on a focusable item, keep the focus on that item.
2. When you click on a non-focusable item, move focus back to the
   Menu.Button or Listbox.Button

We broke part 2, we never returned to the Menu.Button or Listbox.Button.
This is (might) be important for screenreaders so that they don't "get lost",
because if you click on a non-focusable item, the document.body becomes
the active element. Confusing.

* add outside-click to Dialog itself

* update docs
  • Loading branch information
RobinMalfait committed Mar 22, 2021
1 parent c1fe403 commit 2b37043
Show file tree
Hide file tree
Showing 36 changed files with 6,592 additions and 146 deletions.
9 changes: 8 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased - React]

- Nothing yet!
### Added

- Add `Disclosure`, `Disclosure.Button` and `Disclosure.Panel` components ([#220](https://github.com/tailwindlabs/headlessui/pull/220))
- Add `Dialog`, `Dialog.Overlay`, `Dialog.Tile` and `Dialog.Description` components ([#220](https://github.com/tailwindlabs/headlessui/pull/220))
- Add `Portal` component ([#220](https://github.com/tailwindlabs/headlessui/pull/220))
- Add `Switch.Description` component, which adds the `aria-describedby` to the actual Switch ([#220](https://github.com/tailwindlabs/headlessui/pull/220))
- Add `FocusTrap` component ([#220](https://github.com/tailwindlabs/headlessui/pull/220))
- Add `Flyout` component ([#220](https://github.com/tailwindlabs/headlessui/pull/220))

## [Unreleased - Vue]

Expand Down
603 changes: 538 additions & 65 deletions packages/@headlessui-react/README.md

Large diffs are not rendered by default.

110 changes: 110 additions & 0 deletions packages/@headlessui-react/pages/dialog/dialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import React, { useState, Fragment } from 'react'
import { Dialog, Transition } from '@headlessui/react'

export default function Home() {
let [isOpen, setIsOpen] = useState(false)

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>

<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"
>
&#8203;
</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>
</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>
</>
)
}
32 changes: 32 additions & 0 deletions packages/@headlessui-react/pages/disclosure/disclosure.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import React from 'react'
import { Disclosure, Transition } from '@headlessui/react'

export default function Home() {
return (
<div className="flex justify-center w-screen h-full p-12 bg-gray-50">
<div className="w-full max-w-xs mx-auto">
<Disclosure>
{({ open }) => (
<>
<Disclosure.Button>Trigger</Disclosure.Button>

<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"
>
<Disclosure.Panel static className="p-4 bg-white mt-4">
Content
</Disclosure.Panel>
</Transition>
</>
)}
</Disclosure>
</div>
</div>
)
}
101 changes: 101 additions & 0 deletions packages/@headlessui-react/pages/popover/popover.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import React, { forwardRef } from 'react'
import { Popover, Portal } from '@headlessui/react'
import { usePopper } from '../../playground-utils/hooks/use-popper'
import { PropsOf as Props } from '../../src/types'

let Button = forwardRef((props: Props<'button'>, ref) => {
return (
<Popover.Button
ref={ref}
className="px-3 py-2 bg-gray-300 border-2 border-transparent focus:outline-none focus:border-blue-900"
{...props}
/>
)
})

function Link(props: Props<'a'>) {
return (
<a
href="/"
className="px-3 py-2 border-2 border-transparent hover:bg-gray-200 focus:bg-gray-200 focus:outline-none focus:border-blue-900"
{...props}
>
{props.children}
</a>
)
}

export default function Home() {
let options = {
placement: 'bottom-start',
strategy: 'fixed',
modifiers: [],
}

let [reference1, popper1] = usePopper(options)
let [reference2, popper2] = usePopper(options)

let links = ['First', 'Second', 'Third', 'Fourth']

return (
<div className="flex justify-center items-center space-x-12 p-12">
<button>Previous</button>

<Popover.Group as="nav" ar-label="Mythical University" className="flex space-x-3">
<Popover as="div" className="relative">
<Button>Normal</Button>
<Popover.Panel className="absolute flex flex-col w-64 bg-gray-100 border-2 border-blue-900">
{links.map((link, i) => (
<Link key={link} hidden={i === 2}>
Normal - {link}
</Link>
))}
</Popover.Panel>
</Popover>

<Popover as="div" className="relative">
<Button>Focus</Button>
<Popover.Panel
focus
className="absolute flex flex-col w-64 bg-gray-100 border-2 border-blue-900"
>
{links.map((link, i) => (
<Link key={link}>Focus - {link}</Link>
))}
</Popover.Panel>
</Popover>

<Popover as="div" className="relative">
<Button ref={reference1}>Portal</Button>
<Portal>
<Popover.Panel
ref={popper1}
className="flex flex-col w-64 bg-gray-100 border-2 border-blue-900"
>
{links.map(link => (
<Link key={link}>Portal - {link}</Link>
))}
</Popover.Panel>
</Portal>
</Popover>

<Popover as="div" className="relative">
<Button ref={reference2}>Focus in Portal</Button>
<Portal>
<Popover.Panel
ref={popper2}
focus
className="flex flex-col w-64 bg-gray-100 border-2 border-blue-900"
>
{links.map(link => (
<Link key={link}>Focus in Portal - {link}</Link>
))}
</Popover.Panel>
</Portal>
</Popover>
</Popover.Group>

<button>Next</button>
</div>
)
}
Loading

0 comments on commit 2b37043

Please sign in to comment.