Commit Graph

22 Commits

Author SHA1 Message Date
Robin Malfait ae8c253c21 Fix typos (#3086)
* fix a bunch of typos

* fix typos in `@headlessui/vue`
2024-04-08 23:31:50 +02:00
Robin Malfait a50be9255a Forward disabled state to hidden inputs in form-like components (#3004)
* make hidden inputs disabled if the wrapping component is disabled

* add tests to verify disabled hidden form elements

* update changelog
2024-02-21 14:16:31 +01:00
Robin Malfait 0e0277a684 Allow setting custom tabIndex on the <Switch /> component (#2966)
* allow setting a custom `tabIndex` on the `<Switch />` component

* update changelog
2024-02-03 17:31:02 +01:00
Robin Malfait e662f12398 2.0.0 Alpha prep (#2887)
* bump React & React DOM dependencies

* fix typo `TOmitableProps` → `TOmittableProps`

* bump prettier

* run prettier after prettier version bump

* bump TypeScript

* run prettier after TypeScript version bump

* enable `verbatimModuleSyntax`

This ensures all imported types are using the `type` keyword.

* add `type` to type related imports

* add common testing scenarios

Will be used in the new and existing components.

* add script to make Next.js happy

Right now Next.js does barrel file optimization and re-writing imports
to a real path in the `dist` folder. Most of those rewrites don't
actually exist because they have an assumption:

```js
import { FooBar } from '@headlessui/react'
```

is rewritten as:
```js
import { FooBar } from '@headlessui/react/dist/components/foo-bar/foo-bar'
```

This script will make sure these paths exist...

* improve `by` prop, introduce `useByComparator`

This hook has a default implementation when comparing objects. If the
object contains an `id`, then we will compare the objects by their
`id`'s without the user of the library needing to specify `by="id"`.

If the objects don't have an `id` prop, then the default is still to
compare by reference (unless specicified otherwise).

* sync yarn.lock

* rename `Features` to `HiddenFeatures` for `Hidden` component

* rename `Features` to `FocusTrapFeatures` in `FocusTrap` component

* rename `Features` to `RenderFeatures` in `render` util

* add `floating-ui` as a dependency + introduce internal floating related components

* bump Vue dependencies

* ensure scroll bar calculations can't go negative

* improve types in `@headlessui/vue`

* use snapshot tests for `Transition` tests in `@headlessui/vue`

* use snapshot tests for `portal` tests in `@headlessui/vue`

* rename `src/components/transitions/` to `src/components/transition/` (singular)

This is so that we can be consistent with the other components.

* drop custom `toMatchFormattedCss`, prefer snapshot tests instead

* use snapshot tests for `Label` tests in `@headlessui/vue`

* use snapshot tests for `Description` tests in `@headlessui/vue`

* sort exported components in tests for `@headlessui/vue`

* use snapshot tests in `@headlessui/tailwindcss`

* rename `mergeProps` to `mergePropsAdvanced`

This is a more complex version of a soon to be exported `mergeProps`
that we will be using in our components.

* do not expose `aria-labelledby` if it is only referencing itself

* expose boolean state as `kebab-case` instead of `camelCase`

These are the ones being exposed inside `data-headlessui-state="..."`

* expose boolean data attributes

A slot with `{active,focus,hover}` will be exposed as:
```html
<span data-headlessui-state="active focus hover"></span>
```

But also as boolean attributes:
```html
<span data-active data-focus data-hover></span>
```

* improve internal types for `className` in `render` util

* ensure we keep exposed data attributes into account when trying to forward them to the component inside the `Fragment`

* add small typescript type fix

This is internal code, and the public API is not influenced by this
`:any`. It does make TypeScript happy.

* introduce `mergeProps` util to be used in our components

This will help us to merge props, when event handlers are available they
will be merged by wrapping them in a function such that both (or more)
event handlers are called for the same `event`.

* add new internal `Modal` component

* fix: when using `Focus.Previous` with `activeIndex = -1` should start at the end

* prefer `window.scrollY` instead of `window.pageYOffset`

Because `window.pageYOffset` is deprecated.

* add `'use client'` directives on client only components

These components use hooks that won't work in server components and you
will receive an error otherwise.

* drop `import 'client-only'` in favor of the `'use client'` directive

* add React Aria dependencies

* pin beta dependencies

* prettier bump formatting

* improve TypeScript types in tests

* use new Jest matchers instead of deprecated ones

* improve typescript types in Vue

* prefer `useLabelledBy` and `useDescribedBy`

* add internal `DisabledProvider`

* add internal `IdProvider`

* add internal `useDidElementMove` hook

* add internal `useElementSize` hook

* add internal `useIsTouchDevice` hook

* add internal `useActivePress` hook

* use snapshot tests for `Description` tests

* use snapshot tests for `Label` tests

* use snapshot tests for `Portal` tests

* use snapshot tests for `render` tests

* add (private) `Tooltip` component

Currently this one is not ready yet, so its not publicly exposed yet.

* add internal `FormFields` component

This one adds a component to render (hidden) inputs for native form
support. It also ensures that form fields can be hoisted to the end of
the nearest `Field`. If the components are not inside a `Field` they
will be rendered in place.

* add new `Button` component

* add new `Checkbox` component

* add new `DataInteractive` component

* add new `Field` component

* add new `Fieldset` component

* add new `Legend` component

* add new `Input` component

* add new `Select` component

* add new `Textarea` component

* export new components

* WIP

* remove `within: true`

This only makes sense if anything inside the current element receives
focus, which is not the case for `input`, `select`, `textarea` or
`Radio/RadioOption`.

* group focus/hover/active hooks together

* conditionally link anchor panel

* immediately focus the container

* prevent premature disabling of `Listbox`'s floating integration

+ Track whether the button moved or not when disabling such that we can
  disable the transitions earlier.

* improve scroll locking on iOS

* skip hydration tests for now

* skip certain focus trap tests for now

* update CHANGELOG.md

* add missing requires

* drop unused `@ts-expect-error`

* ignore type issues in playgrounds

These playgrounds are mainly test playgrounds. Lower priority for now,
we will get back to them.

* add yarn resolutions to solve swc bug
2023-12-20 19:57:57 +01:00
Robin Malfait f2179f36c0 further improve import sorting 2023-09-11 19:09:53 +02:00
Robin Malfait 76dd10ea55 Sort imports automatically (#2741)
* add `prettier-plugin-organize-imports` and `prettier-plugin-tailwindcss`

* format

* bump Tailwind CSS

* format playgrounds using updated Tailwind CSS and Prettier plugins

* use import syntax
2023-09-11 18:36:30 +02:00
Robin Malfait ac859fe6b1 Submit form on Enter even if no submit-like button was found (#2613)
* `requestSubmit` when a submit-like button cannot be found

* add tests

* update changelog
2023-07-25 15:48:35 +02:00
Arber Sylejmani fb612f7580 Add form prop to form-like components such as RadioGroup, Switch, Listbox, and Combobox (#2356)
* Adds form prop to Switch component

* add `form` prop to `Switch` component in Vue

+ tests for both React and Vue

* add `form` prop to `Combobox` component

* add `form` prop to `Listbox` comopnent

* add `form` prop to `RadioGroup` component

* update changelog

* add Oxford comma

* cleanup `screen` import

---------

Co-authored-by: Robin Malfait <malfait.robin@gmail.com>
2023-03-14 13:36:49 +01:00
Dilshod de7ddf9e45 Enable native label behavior for Switch component (#2265)
* add native label behavior for switch

* Add reference tests for React and Vue

These don’t work in JSDOM so they’re skipped but we can use these to reference expected behavior once we have playwright-based tests

* Fix Vue playground switch example

* Only prevent default when the element is a label

* Port change to Vue

* Update changelog

---------

Co-authored-by: Jordan Pittman <jordan@cryptica.me>
2023-02-28 12:22:37 -05:00
Robin Malfait c0f0d43383 Reset form-like components when the parent <form> resets (#2004)
* add reset button to form example

* refactor React Listbox

This splitsup the raw `[state, dispatch]` to separate `useActions` and `useData` hooks.

This allows us to make the actions themselves simpler and include logic
that doesn't really belong in the reducer itself.

This also allows us to expose data via the `useData` hook that doesn't
belong in the state exposed from the `useReducer` hook.

E.g.: we used to store a `propsRef` from the root `Listbox`, and update
the ref with the new props in a `useEffect`. Now, we will just expose
that information directly via the `useData` hook. This simplifies the
code, removes useEffect's and so on.

* refactor Tabs, ensure function reference stays the same

If the `isControlled` value changes, then the references to all the
functions changed. Now they won't because of the `useEvent` hooks.

* type the actions abg similar to how we type the data bag

* refactor RadioGroup to use useData/useActions hooks

* reset Listbox to defaultValue on form reset

* reset Combobox to defaultValue on form reset

* reset RadioGroup to defaultValue on form reset

* reset Switch to defaultChecked on form reset

* port combinations/form playground example to Vue

* update changelog
2022-11-09 23:39:23 +01:00
Robin Malfait 1831832458 Make form components uncontrollable (#1683)
* implement uncontrolled form components

A few versions ago we introduced compatibility with the native `form`
element. This means that behind the scenes we render hidden inputs that
are kept in sync which allows you to submit your normal form and get
data via `new FormData(event.currentTarget)`.

Before this change every form related component (Switch, RadioGroup,
Listbox and Combobox) always had to be passed a `value` and an
`onChange` regardless of this change.

This change will allow you to not even use the `value` and the
`onChange` at all and keep it completely uncontrolled.

This has some changes:

- `value` is made optional
- `onChange` is made optional (but will still be called if passed
  regardless of being controlled or uncontrolled)
- `defaultValue` got added so that you can still pre-fill your values
  with known values.
- `value` render prop got exposed so that you can still use this while
  rendering.

This should also make it completely compatible with tools like Remix
without wiring up your own state.

* update example combinations/form playground to use uncontrolled
components

* improve types, add missing render prop arguments

* add tests for uncontrolled components (React)

* implement uncontrolled form elements in Vue
2022-08-01 12:37:50 +02:00
Robin Malfait 0162c57d88 add React 18 compatibility (#1326)
* bump dev dependencies to React 18

* setup Jest to include `IS_REACT_ACT_ENVIRONMENT`

* prefer `useId` from React 18 if it exists

In React 16 & 17, where `useId` doesn't exist, we will fallback to our
implementation we have been using up until now.

The `useId` exposed by React 18, ensures stable references even in SSR
environments.

* update expected events

React 18 now uses the proper events:
- `blur` -> `focusout`
- `focus` -> `focusin`

* ensure to wait a bit longer

This is a bit unfortunate, but since React 18 now does an extra
unmount/remount in `StrictMode` to ensure that your code is
ConcurrentMode ready, it takes a bit longer to settle what the DOM sees.

That said, this is a temporary "hack". We are going to experiment with
using tools like Puppeteer/Playwright to run our tests in an actual
browser instead to eliminate all the weird details that we have to keep
in mind.

* prefer `.focus()` over `fireEvent.focus(el)`

* abstract `microTask` polyfill code

* prefer our `focus(el)` function over `el.focus()`

Internally we would still use `el.focus()`, but this allows us to have
more control over that `focus` function.

* add React 18 to the React Playground

* improve hooks for React 18

- Improving the cleanup of useEffect hooks
- useIsoMorphicEffect instead of normal useEffect, so that we can use
  useLayoutEffect to be a bit quicker.

* improve disposables

- This allows us to add event listeners on a node, and get automatic
  cleanup once `dispose` gets called.
- We also return all the `d.add` calls, so that we can cleanup specific
  parts only instead of everything or nothing.

* reimplement the Transition component to be React 18 ready

* wait an additional frame for everything to settle

* update playground examples

* suppressConsoleLogs for RadioGroup components

* update changelog

* keep the `to` classes for a smoother transition

In the next transition we will remove _all_ classes provided and re-add
the once we need.

---

Some extra special thanks:

- Thanks @silvenon for your initial work on the `transition` events in #926
- Thanks @thecrypticace for doing late-night debugging sessions

Co-authored-by: =?UTF-8?q?Matija=20Marohni=C4=87?= <matija.marohnic@gmail.com>
Co-authored-by: Jordan Pittman <jordan@cryptica.me>
2022-04-13 22:07:01 +02:00
Robin Malfait c475cab451 Allow Enter for form submit in RadioGroup, Switch and Combobox improvements (#1285)
* improve rendering of hidden form fields

* add `attemptSubmit` helper

This will allow us to _try_ and submit a form based on any element you
pass it. It will try and lookup the current form and if it is
submittable it will attempt to submit it.

Instead of submitting the form directly, we try to follow the native
browser support where it looks for the first `input[type=submit]`,
`input[type=image]`, `button` or `button[type=submit]`, then it clicks
it.

This allows you to disable your submit button, or have an `onClick` that
does an `event.preventDefault()` just like the native form in a browser
would do.

* ensure we can submit a form from a closed Combobox

When the Combobox is closed, then the `Enter` keydown event will be
ignored and thus not use `event.preventDefault()`.

With recent changes where we always have an active option, it means that
you will always be able to select an option.

If we have no option at all (some edge case) or when the combobox is
closed, then the `Enter` keydown event will just bubble, allowing you to
submit a form.

Fixes: #1282

This is a continuation of a PR ([#1176](https://github.com/tailwindlabs/headlessui/pull/1176)) provided by Alexander, so wanted to include
them as a co-author because of their initial work.

Co-authored-by: Alexander Lyon <arlyon@me.com>

* ensure we can submit a form from a RadioGroup

* ensure we can submit a form from a Switch

* simplify / refactor form playground example

* update changelog

Co-authored-by: Alexander Lyon <arlyon@me.com>
2022-03-31 21:42:34 +02:00
Robin Malfait 7bb89871ba Add <form> compatibility (#1214)
* implement `objetToFormEntries` functionality

If we are working with more complex data structures then we have to
encode those data structures into a syntax that the HTML can understand.

This means that we have to use `<input type="hidden" name="..." value="...">` syntax.

To convert a simple array we can use the following syntax:
```js
// Assuming we have a `name` of `person`
let input = ['Alice', 'Bob', 'Charlie']
```

Results in:
```html
<input type="hidden" name="person[]" value="Alice" />
<input type="hidden" name="person[]" value="Bob" />
<input type="hidden" name="person[]" value="Charlie" />
```

Note: the additional `[]` in the name attribute.

---

A more complex object (even deeply nested) can be encoded like this:
```js
// Assuming we have a `name` of `person`
let input = {
  id: 1,
  name: {
    first: 'Jane',
    last: 'Doe'
  }
}
```

Results in:
```html
<input type="hidden" name="person[id]" value="1" />
<input type="hidden" name="person[name][first]" value="Jane" />
<input type="hidden" name="person[name][last]" value="Doe" />
```

* implement VisuallyHidden component

* implement and export some extra helper utilities

* implement form element for Switch

* implement form element for Combobox

* implement form element for RadioGroup

* implement form element for Listbox

* add combined forms example to the playground

* update changelog

* enable support for iterators

* ensure to compile dom iterables

* remove unused imports
2022-03-09 11:24:45 +01:00
Robin Malfait fdd2629795 Improve overal codebase, use modern tech like esbuild and TypeScript 4! (#1055)
* use esbuild for React instead of tsdx

* remove tsdx from Vue

* use consistent names

* add jest and prettier

* update scripts

* ignore some folders for prettier

* run lint script instead of tsdx lint

* run prettier en-masse

This has a few changes because of the new prettier version.

* bump typescript to latest version

* make typescript happy

* cleanup playground package.json

* make esbuild a dev dependency

* make scripts consistent

* fix husky hooks

* add dedicated watch script

* add `yarn playground-react` and `yarn react-playground` (alias)

This will make sure to run a watcher for the actual @headlessui/react
package, and start a development server in the playground-react package.

* ignore formatting in the .next folder

* run prettier on playground-react package

* setup playground-vue

Still not 100% working, but getting there!

* add playground aliases in @headlessui/vue and @headlessui/react

This allows you to run `yarn react playground` or `yarn vue playground`
from the root.

* add `clean` script

* move examples folder in playground-vue to root

* ensure new lines for consistency in scripts

* fix typescript issue

* fix typescript issues in playgrounds

* make sure to run prettier on everything it can

* run prettier on all files

* improve error output

If you minify the code, then it could happen that the errors are a bit
obscure. This will hardcode the component name to improve errors.

* add the `prettier-plugin-tailwindcss` plugin, party!

* update changelog
2022-01-27 17:07:38 +01:00
Robin Malfait c1117840fd Only add type=button for real buttons (#709)
* add `{type:'button'}` only for buttons

We will try and infer the type based on the passed in `props.as` prop or
the default tag. However, when somebody uses `as={CustomComponent}` then
we don't know what it will render. Therefore we have to pass it a ref
and check if the final result is a button or not. If it is, and it
doesn't have a `type` yet, then we can set the `type` correctly.

* update changelog
2021-08-02 13:57:58 +02:00
Robin Malfait 11a5942142 use passive instead of clickable for Switch label (#332) 2021-04-13 18:45:51 +02:00
Robin Malfait a02c818f94 Use internal label and descriptions (#313)
* improve internal Label component

We will now add a name to improve error messages, we also introduced a
`clickable` prop on the label.

Not 100% happy with the implementation of these internal Label &
Description components, but they are internal so we can always change it
to something that makes more sense!

* improve internal Description component

We will now add a name to improve error messages.

* provide the name prop to Description & Label providers

* implement the useLabels and useDescriptions in the Switch components

* update documentation
2021-04-08 17:39:02 +02:00
Robin Malfait 648a2843e6 Multiple new components (#220)
* 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
2021-04-02 15:55:14 +02:00
Robin Malfait ef00732685 cleanup and consistency (#213)
- Made the use of `const` and `let` consistent
- import required functions and types from 'react' instead of using the
  `React.` namespace.
- Added `Expand` type, which can expand complex types to their "final"
  result.
- Ensured that we use `as const` for DEFAULT_XXX_TAG where we used a
  string. So that we have the type of `div` instead of `string` for
  example.
- Used `interface` over `type` where possible. I'm personally more of a
  `type` fan. But the TypeScript recommends `interfaces` where possible
  because they are faster, yield better error messages and so on.
2021-01-30 14:46:54 +01:00
Robin Malfait fecd61dff6 ensure that you can't use Enter to invoke the Switch
And a bunch of keyPress and keyboard related shenanigans
2020-10-06 14:00:01 +02:00
Robin Malfait 6e3d496998 feat: add Switch component (#26)
* add Switch component

* add tests to verify that we can click the label to toggle the Switch

* use onKeyUp to prevent triggering the onClick in firefox
2020-10-05 16:47:31 +02:00