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

Multiple new components #220

Merged
merged 47 commits into from
Feb 18, 2021
Merged

Multiple new components #220

merged 47 commits into from
Feb 18, 2021

Conversation

RobinMalfait
Copy link
Member

@RobinMalfait RobinMalfait commented Feb 1, 2021

This PR adds a few new components:

  • Dialog
  • Disclosure
  • FocusTrap
  • Popover
  • Portal
  • Switch.Description (This adds an aria-describedby to the Switch component)

This PR also has a few fixes and perforamnce improvements:

  • Internally we use useReducer a lot, and state updates were always causing re-renders. Now we will only re-render when something actually changed. This change has not been applied to all components yet, but didn't want to bloat the scope to much in this very big PR
  • "outside click" had a few regression, I applied fixes to the Menu and Listbox components
  • Improvements on the internal useSyncRefs, we created a new callback when the refs changed, but they changed always
  • Introduced an internal set of utilities for focus management

Disclosure component

A component for showing/hiding content.

Disclosure

<Disclosure>
  <Disclosure.Button>Toggle</Disclosure.Button>
  <Disclosure.Panel>Contents</Disclosure.Panel>
</Disclosure>

Props

Prop Type Default Description
as String | Component React.Fragment (no wrapper element) The element or component the Disclosure should render as.

Render prop object

Prop Type Description
open Boolean Whether or not the disclosure is open.

Disclosure.Button

Props

Prop Type Default Description
as String | Component button The element or component the Disclosure.Button should render as.

Render prop object

Prop Type Description
open Boolean Whether or not the disclosure is open.

Disclosure.Panel

Props

Prop Type Default Description
as String | Component div The element or component the Disclosure.Panel should render as.
static Boolean false Whether the element should ignore the internally managed open/closed state.
unmount Boolean true Whether the element should be unmounted or hidden based on the open/closed state.

note: static and unmount can not be used at the same time. You will get a TypeScript error if you try to do it.

Render prop object

Prop Type Description
open Boolean Whether or not the disclosure is open.

FocusTrap component

A component for making sure that you can't Tab out of the contents of this
component.

Focus strategy:

  • An initialFocus prop can be passed in, this is a ref object, which is a ref to the element that should receive initial focus.
  • If an input element exists with an autoFocus prop, it will receive initial focus.
  • If none of those exists, it will try and focus the first focusable element.
  • If that doesn't exist, it will throw an error.

Once the FocusTrap will unmount, the focus will be restored to the element that was focused before the FocusTrap was rendered.

FocusTrap

<FocusTrap>
  <form>
    <input type="email" name="Email" />
    <input type="password" name="password" />
    <button>Submit</button>
  </form>
</FocusTrap>

Props

Prop Type Default Description
as String | Component div The element or component the FocusTrap should render as.
initialFocus React.MutableRefObject undefined A ref to an element that should receive focus first.

Portal component

A component for rendering your contents within a Portal (at the end of document.body).

Portal

<Portal>
  <p>This will be rendered inside a Portal, at the end of `document.body`</p>
</Portal>

Dialog component

This component can be used to render content inside a Dialog/Modal. This contains a ton of features:

  1. Renders inside a Portal
  2. Controlled component
  3. Uses FocusTrap with its features (Focus first focusable element, autoFocus or initialFocus ref)
  4. Adds a scroll lock
  5. Prevents content jumps by faking your scrollbar width
  6. Marks other elements as inert (hides other elements from screen readers)
  7. Closes on escape
  8. Closes on click outside
  9. Once the Dialog becomes hidden (e.g.: md:hidden) it will also trigger the onClose

Dialog

function Example() {
  let [isOpen, setIsOpen] = useState(true)

  return (
    <Dialog open={isOpen} onClose={setIsOpen}>
      <Dialog.Overlay />

      <Dialog.Title>Deactivate account</Dialog.Title>
      <Dialog.Description>This will permanently deactivate your account</Dialog.Description>

      <p>
        Are you sure you want to deactivate your account? All of your data will be permanently
        removed. This action cannot be undone.
      </p>

      <button onClick={() => setIsOpen(false)}>Deactivate</button>
      <button onClick={() => setIsOpen(false)}>Cancel</button>
    </Dialog>
  )
}

Props

Prop Type Default Description
open Boolean / Wether the Dialog is open or not.
onClose Function / Called when the Dialog should close. For convenience we pass in a onClose(false) so that you can use: onClose={setIsOpen}.
initialFocus React.MutableRefObject / A ref to an element that should receive focus first.
as String | Component div The element or component the Dialog should render as.
static Boolean false Whether the element should ignore the internally managed open/closed state.
unmount Boolean true Whether the element should be unmounted or hidden based on the open/closed state.

note: static and unmount can not be used at the same time. You will get a TypeScript error if you try to do it.

Render prop object

Prop Type Description
open Boolean Whether or not the dialog is open.

Dialog.Overlay

This can be used to create an overlay for your Dialog component. Clicking on the overlay will close the Dialog.

Props

Prop Type Default Description
as String | Component div The element or component the Dialog.Overlay should render as.

Render prop object

Prop Type Description
open Boolean Whether or not the disclosure is open.

Dialog.Title

This is the title for your Dialog. When this is used, it will set the aria-labelledby on the Dialog.

Props

Prop Type Default Description
as String | Component h2 The element or component the Dialog.Title should render as.

Render prop object

Prop Type Description
open Boolean Whether or not the disclosure is open.

Dialog.Description

This is the description for your Dialog. When this is used, it will set the aria-describedby on the Dialog.

Props

Prop Type Default Description
as String | Component p The element or component the Dialog.Description should render as.

Render prop object

Prop Type Description
open Boolean Whether or not the disclosure is open.

Popover component

This component can be used for navigation menu's, mobile menu's and flyout menu's.

Popover

<Popover.Group>
  <Popover>
    <Popover.Button>Solutions</Popover.Button>
    <Popover.Panel>
      <a href="#">Analytics</a>
      <a href="#">Engagement</a>
      <a href="#">Security</a>
      <a href="#">Integrations</a>
      <a href="#">Automations</a>
    </Popover.Panel>
  </Popover>

  <a href="#">Pricing</a>
  <a href="#">Docs</a>

  <Popover>
    <Popover.Button>More</Popover.Button>
    <Popover.Panel focus>
      <a href="#">Help Center</a>
      <a href="#">Guides</a>
      <a href="#">Events</a>
      <a href="#">Security</a>
    </Popover.Panel>
  </Popover>
</Popover.Group>

Props

Prop Type Default Description
as String | Component div The element or component the Popover should render as.

Render prop object

Prop Type Description
open Boolean Whether or not the dialog is open.

Popover.Overlay

This can be used to create an overlay for your Popover component. Clicking on the overlay will close the Popover.

Props

Prop Type Default Description
as String | Component div The element or component the Popover.Overlay should render as.

Render prop object

Prop Type Description
open Boolean Whether or not the disclosure is open.

Popover.Button

This is the trigger component to open a Popover. You can also use this
Popover.Button component inside a Popover.Panel, if you do so, then it will
behave as a close button. We will also make sure to provide the correct
aria-* attributes onto the button.

Props

Prop Type Default Description
as String | Component button The element or component the Popover.Button should render as.

Render prop object

Prop Type Description
open Boolean Whether or not the disclosure is open.

Popover.Panel

This component contains the contents of your Popover.

Props

Prop Type Default Description
as String | Component div The element or component the Popover.Panel should render as.
focus Boolean false This will force focus inside the Popover.Panel when the Popover is open. It will also close the Popover if focus left this component.
static Boolean false Whether the element should ignore the internally managed open/closed state.
unmount Boolean true Whether the element should be unmounted or hidden based on the open/closed state.

note: static and unmount can not be used at the same time. You will get a TypeScript error if you try to do it.

Render prop object

Prop Type Description
open Boolean Whether or not the disclosure is open.

Popover.Group

This allows you to wrap multiple elements and Popover's inside a group.

  • When you tab out of a Popover.Panel, it will focus the next Popover.Button in line.
  • If focus left the Popover.Group it will close all the Popover's.

Props

Prop Type Default Description
as String | Component div The element or component the Popover.Group should render as.

@vercel
Copy link

vercel bot commented Feb 1, 2021

This pull request is being automatically deployed with Vercel (learn more).
To see the status of your deployments, click below or on the icon next to each commit.

headlessui-react – ./packages/@headlessui-react

🔍 Inspect: https://vercel.com/tailwindlabs/headlessui-react/q8qu5m3kp
✅ Preview: https://headlessui-react-git-multiple-components.tailwindlabs.vercel.app

headlessui-vue – ./packages/@headlessui-vue

🔍 Inspect: https://vercel.com/tailwindlabs/headlessui-vue/p4r4zyvz3
✅ Preview: https://headlessui-vue-git-multiple-components.tailwindlabs.vercel.app

@RobinMalfait RobinMalfait changed the title multiple components Multiple new components Feb 1, 2021
@RobinMalfait RobinMalfait merged commit 524fdb1 into develop Feb 18, 2021
@RobinMalfait RobinMalfait deleted the multiple-components branch February 18, 2021 17:27
RobinMalfait added a commit that referenced this pull request Feb 18, 2021
* 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
@Zertz
Copy link
Contributor

Zertz commented Feb 22, 2021

This looks very promising! If the work isn't quite ready and you can spare the time, do you think it would be possible to release a beta on npm?

@RobinMalfait
Copy link
Member Author

@Zertz You can already try it using npm install @headlessui/react@dev or yarn add @headlessui/react@dev.

This was referenced Feb 23, 2021
This was referenced Mar 15, 2021
RobinMalfait added a commit that referenced this pull request Mar 22, 2021
* 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
RobinMalfait added a commit that referenced this pull request Mar 26, 2021
* 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
RobinMalfait added a commit that referenced this pull request Apr 2, 2021
* 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
RobinMalfait added a commit that referenced this pull request Apr 2, 2021
* 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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants