From e662f1239831c849f64adecbb38cf465d18a7ee2 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Wed, 20 Dec 2023 19:57:57 +0100 Subject: [PATCH] 2.0.0 Alpha prep (#2887) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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 ``` But also as boolean attributes: ```html ``` * 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 --- package.json | 12 +- packages/@headlessui-react/CHANGELOG.md | 29 +- packages/@headlessui-react/package.json | 14 +- .../src/components/button/button.test.tsx | 39 + .../src/components/button/button.tsx | 88 + .../src/components/checkbox/checkbox.test.tsx | 121 + .../src/components/checkbox/checkbox.tsx | 193 + .../combobox-button/combobox-button.tsx | 3 + .../combobox-input/combobox-input.tsx | 3 + .../combobox-label/combobox-label.tsx | 3 + .../combobox-option/combobox-option.tsx | 3 + .../combobox-options/combobox-options.tsx | 3 + .../src/components/combobox/combobox.test.tsx | 216 +- .../src/components/combobox/combobox.tsx | 534 +- .../data-interactive.test.tsx | 52 + .../data-interactive/data-interactive.tsx | 67 + .../__snapshots__/description.test.tsx.snap | 45 + .../description/description.test.tsx | 43 +- .../components/description/description.tsx | 35 +- .../dialog-backdrop/dialog-backdrop.tsx | 3 + .../dialog-description/dialog-description.tsx | 3 + .../dialog-overlay/dialog-overlay.tsx | 3 + .../components/dialog-panel/dialog-panel.tsx | 3 + .../components/dialog-title/dialog-title.tsx | 3 + .../src/components/dialog/dialog.test.tsx | 127 +- .../src/components/dialog/dialog.tsx | 137 +- .../disclosure-button/disclosure-button.tsx | 3 + .../disclosure-panel/disclosure-panel.tsx | 3 + .../components/disclosure/disclosure.test.tsx | 42 +- .../src/components/disclosure/disclosure.tsx | 129 +- .../src/components/field/field.test.tsx | 42 + .../src/components/field/field.tsx | 73 + .../src/components/fieldset/fieldset.test.tsx | 90 + .../src/components/fieldset/fieldset.tsx | 61 + .../focus-trap-features.tsx | 3 + .../components/focus-trap/focus-trap.test.tsx | 29 +- .../src/components/focus-trap/focus-trap.tsx | 113 +- .../src/components/input/input.test.tsx | 17 + .../src/components/input/input.tsx | 97 + .../label/__snapshots__/label.test.tsx.snap | 45 + .../src/components/label/label.test.tsx | 41 +- .../src/components/label/label.tsx | 132 +- .../src/components/legend/legend.tsx | 34 + .../listbox-button/listbox-button.tsx | 3 + .../listbox-label/listbox-label.tsx | 3 + .../listbox-option/listbox-option.tsx | 3 + .../listbox-options/listbox-options.tsx | 3 + .../listbox-selected-option.tsx | 3 + .../src/components/listbox/listbox.test.tsx | 193 +- .../src/components/listbox/listbox.tsx | 799 ++- .../components/menu-button/menu-button.tsx | 3 + .../components/menu-heading/menu-heading.tsx | 3 + .../src/components/menu-item/menu-item.tsx | 3 + .../src/components/menu-items/menu-items.tsx | 3 + .../components/menu-section/menu-section.tsx | 3 + .../menu-separator/menu-separator.tsx | 3 + .../src/components/menu/menu.test.tsx | 316 +- .../src/components/menu/menu.tsx | 511 +- .../popover-button/popover-button.tsx | 3 + .../popover-group/popover-group.tsx | 3 + .../popover-overlay/popover-overlay.tsx | 3 + .../popover-panel/popover-panel.tsx | 3 + .../src/components/popover/popover.test.tsx | 48 +- .../src/components/popover/popover.tsx | 330 +- .../portal/__snapshots__/portal.test.tsx.snap | 3 + .../src/components/portal/portal.test.tsx | 4 +- .../src/components/portal/portal.tsx | 36 +- .../radio-group-description.tsx | 3 + .../radio-group-label/radio-group-label.tsx | 3 + .../radio-group-option/radio-group-option.tsx | 3 + .../radio-group/radio-group.test.tsx | 94 +- .../components/radio-group/radio-group.tsx | 343 +- .../src/components/radio/radio.tsx | 3 + .../src/components/select/select.test.tsx | 27 + .../src/components/select/select.tsx | 99 + .../switch-description/switch-description.tsx | 3 + .../components/switch-group/switch-group.tsx | 3 + .../components/switch-label/switch-label.tsx | 3 + .../src/components/switch/switch.test.tsx | 10 +- .../src/components/switch/switch.tsx | 156 +- .../src/components/tab-group/tab-group.tsx | 3 + .../src/components/tab-list/tab-list.tsx | 3 + .../src/components/tab-panel/tab-panel.tsx | 3 + .../src/components/tab-panels/tab-panels.tsx | 3 + .../src/components/tab/tab.tsx | 3 + .../src/components/tabs/tabs.ssr.test.tsx | 3 +- .../src/components/tabs/tabs.test.tsx | 52 +- .../src/components/tabs/tabs.tsx | 125 +- .../src/components/textarea/textarea.test.tsx | 17 + .../src/components/textarea/textarea.tsx | 94 + .../src/components/tooltip/tooltip.tsx | 499 ++ .../transition-child/transition-child.tsx | 3 + .../__snapshots__/transition.test.tsx.snap | 144 + .../transition.ssr.test.tsx | 0 .../transition.test.tsx | 467 +- .../src/components/transition/transition.tsx | 644 ++ .../utils/transition.test.ts | 0 .../utils/transition.ts | 16 +- .../src/components/transitions/transition.tsx | 636 +- .../adjust-scrollbar-padding.ts | 11 +- .../document-overflow/handle-ios-locking.ts | 100 +- .../hooks/document-overflow/overflow-store.ts | 2 +- .../hooks/document-overflow/prevent-scroll.ts | 2 +- .../use-document-overflow.ts | 2 +- .../src/hooks/use-active-press.tsx | 98 + .../src/hooks/use-by-comparator.ts | 34 + .../src/hooks/use-did-element-move.ts | 28 + .../src/hooks/use-element-size.ts | 36 + .../@headlessui-react/src/hooks/use-event.ts | 2 +- .../@headlessui-react/src/hooks/use-id.ts | 1 - .../src/hooks/use-inert.test.tsx | 2 +- .../@headlessui-react/src/hooks/use-inert.tsx | 2 +- .../src/hooks/use-is-touch-device.ts | 24 + .../src/hooks/use-iso-morphic-effect.ts | 2 +- .../src/hooks/use-outside-click.ts | 2 +- .../src/hooks/use-root-containers.tsx | 4 +- .../@headlessui-react/src/hooks/use-store.ts | 2 +- .../src/hooks/use-transition.ts | 4 +- packages/@headlessui-react/src/index.test.ts | 76 +- packages/@headlessui-react/src/index.ts | 17 +- .../src/internal/disabled.tsx | 14 + .../src/internal/floating.tsx | 562 ++ .../src/internal/focus-sentinel.tsx | 4 +- .../src/internal/form-fields.tsx | 102 + .../@headlessui-react/src/internal/hidden.tsx | 21 +- .../@headlessui-react/src/internal/id.tsx | 11 + .../@headlessui-react/src/internal/modal.tsx | 218 + .../test-utils/accessibility-assertions.ts | 198 +- .../src/test-utils/fake-pointer.ts | 5 +- .../src/test-utils/interactions.test.tsx | 2 +- .../src/test-utils/interactions.ts | 2 +- .../src/test-utils/scenarios.tsx | 278 + packages/@headlessui-react/src/types.ts | 29 +- .../utils/__snapshots__/render.test.tsx.snap | 241 + .../src/utils/calculate-active-index.ts | 3 + .../src/utils/focus-management.ts | 47 +- packages/@headlessui-react/src/utils/owner.ts | 2 +- .../src/utils/render.test.tsx | 199 +- .../@headlessui-react/src/utils/render.ts | 184 +- .../src/utils/start-transition.ts | 1 - packages/@headlessui-react/tsconfig.json | 3 +- .../@headlessui-tailwindcss/jest.config.cjs | 1 - .../@headlessui-tailwindcss/jest.setup.js | 54 - .../src/__snapshots__/index.test.ts.snap | 49 + .../@headlessui-tailwindcss/src/index.test.ts | 54 +- packages/@headlessui-vue/package.json | 8 +- .../src/components/combobox/combobox.test.ts | 20 +- .../src/components/combobox/combobox.ts | 36 +- .../__snapshots__/description.test.ts.snap | 11 + .../description/description.test.ts | 57 +- .../src/components/dialog/dialog.test.ts | 6 +- .../components/disclosure/disclosure.test.ts | 4 +- .../components/focus-trap/focus-trap.test.ts | 2 +- .../src/components/focus-trap/focus-trap.ts | 6 +- .../label/__snapshots__/label.test.ts.snap | 11 + .../src/components/label/label.test.ts | 57 +- .../src/components/listbox/listbox.test.tsx | 10 +- .../src/components/listbox/listbox.ts | 8 +- .../src/components/menu/menu.test.tsx | 6 +- .../src/components/menu/menu.ts | 2 +- .../src/components/popover/popover.test.ts | 4 +- .../src/components/popover/popover.ts | 6 +- .../portal/__snapshots__/portal.test.ts.snap | 3 + .../src/components/portal/portal.test.ts | 13 +- .../src/components/portal/portal.ts | 8 +- .../radio-group/radio-group.test.ts | 2 +- .../src/components/radio-group/radio-group.ts | 6 +- .../src/components/switch/switch.test.tsx | 4 +- .../src/components/switch/switch.ts | 4 +- .../src/components/tabs/tabs.test.ts | 2 +- .../src/components/tabs/tabs.ts | 12 +- .../__snapshots__/transition.test.ts.snap | 450 ++ .../components/transitions/transition.test.ts | 449 +- .../src/components/transitions/transition.ts | 4 +- .../adjust-scrollbar-padding.ts | 11 +- .../document-overflow/handle-ios-locking.ts | 2 +- .../hooks/document-overflow/overflow-store.ts | 2 +- .../hooks/document-overflow/prevent-scroll.ts | 2 +- .../use-document-overflow.ts | 4 +- .../src/hooks/use-controllable.ts | 2 +- .../src/hooks/use-inert.test.ts | 2 +- .../@headlessui-vue/src/hooks/use-inert.ts | 2 +- .../src/hooks/use-root-containers.ts | 2 +- .../@headlessui-vue/src/hooks/use-store.ts | 2 +- .../src/hooks/use-text-value.ts | 2 +- packages/@headlessui-vue/src/index.test.ts | 38 +- .../src/test-utils/fake-pointer.ts | 5 +- .../src/test-utils/interactions.test.ts | 2 +- packages/@headlessui-vue/src/utils/dom.ts | 2 +- packages/@headlessui-vue/src/utils/owner.ts | 4 +- packages/@headlessui-vue/tsconfig.json | 3 +- packages/playground-react/data.ts | 1 - packages/playground-react/package.json | 7 +- .../combobox-virtual-with-empty-states.tsx | 117 +- .../pages/combobox/combobox-virtualized.tsx | 81 +- .../pages/popover/popover.tsx | 2 +- packages/playground-vue/index.html | 2 +- packages/playground-vue/src/data.ts | 1 - packages/playground-vue/src/main.ts | 1 + scripts/build.sh | 5 + scripts/make-nextjs-happy.js | 51 + scripts/release-channel.js | 3 + yarn.lock | 5165 +++++++++-------- 203 files changed, 11754 insertions(+), 6201 deletions(-) create mode 100644 packages/@headlessui-react/src/components/button/button.test.tsx create mode 100644 packages/@headlessui-react/src/components/button/button.tsx create mode 100644 packages/@headlessui-react/src/components/checkbox/checkbox.test.tsx create mode 100644 packages/@headlessui-react/src/components/checkbox/checkbox.tsx create mode 100644 packages/@headlessui-react/src/components/combobox-button/combobox-button.tsx create mode 100644 packages/@headlessui-react/src/components/combobox-input/combobox-input.tsx create mode 100644 packages/@headlessui-react/src/components/combobox-label/combobox-label.tsx create mode 100644 packages/@headlessui-react/src/components/combobox-option/combobox-option.tsx create mode 100644 packages/@headlessui-react/src/components/combobox-options/combobox-options.tsx create mode 100644 packages/@headlessui-react/src/components/data-interactive/data-interactive.test.tsx create mode 100644 packages/@headlessui-react/src/components/data-interactive/data-interactive.tsx create mode 100644 packages/@headlessui-react/src/components/description/__snapshots__/description.test.tsx.snap create mode 100644 packages/@headlessui-react/src/components/dialog-backdrop/dialog-backdrop.tsx create mode 100644 packages/@headlessui-react/src/components/dialog-description/dialog-description.tsx create mode 100644 packages/@headlessui-react/src/components/dialog-overlay/dialog-overlay.tsx create mode 100644 packages/@headlessui-react/src/components/dialog-panel/dialog-panel.tsx create mode 100644 packages/@headlessui-react/src/components/dialog-title/dialog-title.tsx create mode 100644 packages/@headlessui-react/src/components/disclosure-button/disclosure-button.tsx create mode 100644 packages/@headlessui-react/src/components/disclosure-panel/disclosure-panel.tsx create mode 100644 packages/@headlessui-react/src/components/field/field.test.tsx create mode 100644 packages/@headlessui-react/src/components/field/field.tsx create mode 100644 packages/@headlessui-react/src/components/fieldset/fieldset.test.tsx create mode 100644 packages/@headlessui-react/src/components/fieldset/fieldset.tsx create mode 100644 packages/@headlessui-react/src/components/focus-trap-features/focus-trap-features.tsx create mode 100644 packages/@headlessui-react/src/components/input/input.test.tsx create mode 100644 packages/@headlessui-react/src/components/input/input.tsx create mode 100644 packages/@headlessui-react/src/components/label/__snapshots__/label.test.tsx.snap create mode 100644 packages/@headlessui-react/src/components/legend/legend.tsx create mode 100644 packages/@headlessui-react/src/components/listbox-button/listbox-button.tsx create mode 100644 packages/@headlessui-react/src/components/listbox-label/listbox-label.tsx create mode 100644 packages/@headlessui-react/src/components/listbox-option/listbox-option.tsx create mode 100644 packages/@headlessui-react/src/components/listbox-options/listbox-options.tsx create mode 100644 packages/@headlessui-react/src/components/listbox-selected-option/listbox-selected-option.tsx create mode 100644 packages/@headlessui-react/src/components/menu-button/menu-button.tsx create mode 100644 packages/@headlessui-react/src/components/menu-heading/menu-heading.tsx create mode 100644 packages/@headlessui-react/src/components/menu-item/menu-item.tsx create mode 100644 packages/@headlessui-react/src/components/menu-items/menu-items.tsx create mode 100644 packages/@headlessui-react/src/components/menu-section/menu-section.tsx create mode 100644 packages/@headlessui-react/src/components/menu-separator/menu-separator.tsx create mode 100644 packages/@headlessui-react/src/components/popover-button/popover-button.tsx create mode 100644 packages/@headlessui-react/src/components/popover-group/popover-group.tsx create mode 100644 packages/@headlessui-react/src/components/popover-overlay/popover-overlay.tsx create mode 100644 packages/@headlessui-react/src/components/popover-panel/popover-panel.tsx create mode 100644 packages/@headlessui-react/src/components/portal/__snapshots__/portal.test.tsx.snap create mode 100644 packages/@headlessui-react/src/components/radio-group-description/radio-group-description.tsx create mode 100644 packages/@headlessui-react/src/components/radio-group-label/radio-group-label.tsx create mode 100644 packages/@headlessui-react/src/components/radio-group-option/radio-group-option.tsx create mode 100644 packages/@headlessui-react/src/components/radio/radio.tsx create mode 100644 packages/@headlessui-react/src/components/select/select.test.tsx create mode 100644 packages/@headlessui-react/src/components/select/select.tsx create mode 100644 packages/@headlessui-react/src/components/switch-description/switch-description.tsx create mode 100644 packages/@headlessui-react/src/components/switch-group/switch-group.tsx create mode 100644 packages/@headlessui-react/src/components/switch-label/switch-label.tsx create mode 100644 packages/@headlessui-react/src/components/tab-group/tab-group.tsx create mode 100644 packages/@headlessui-react/src/components/tab-list/tab-list.tsx create mode 100644 packages/@headlessui-react/src/components/tab-panel/tab-panel.tsx create mode 100644 packages/@headlessui-react/src/components/tab-panels/tab-panels.tsx create mode 100644 packages/@headlessui-react/src/components/tab/tab.tsx create mode 100644 packages/@headlessui-react/src/components/textarea/textarea.test.tsx create mode 100644 packages/@headlessui-react/src/components/textarea/textarea.tsx create mode 100644 packages/@headlessui-react/src/components/tooltip/tooltip.tsx create mode 100644 packages/@headlessui-react/src/components/transition-child/transition-child.tsx create mode 100644 packages/@headlessui-react/src/components/transition/__snapshots__/transition.test.tsx.snap rename packages/@headlessui-react/src/components/{transitions => transition}/transition.ssr.test.tsx (100%) rename packages/@headlessui-react/src/components/{transitions => transition}/transition.test.tsx (74%) create mode 100644 packages/@headlessui-react/src/components/transition/transition.tsx rename packages/@headlessui-react/src/components/{transitions => transition}/utils/transition.test.ts (100%) rename packages/@headlessui-react/src/components/{transitions => transition}/utils/transition.ts (91%) create mode 100644 packages/@headlessui-react/src/hooks/use-active-press.tsx create mode 100644 packages/@headlessui-react/src/hooks/use-by-comparator.ts create mode 100644 packages/@headlessui-react/src/hooks/use-did-element-move.ts create mode 100644 packages/@headlessui-react/src/hooks/use-element-size.ts create mode 100644 packages/@headlessui-react/src/hooks/use-is-touch-device.ts create mode 100644 packages/@headlessui-react/src/internal/disabled.tsx create mode 100644 packages/@headlessui-react/src/internal/floating.tsx create mode 100644 packages/@headlessui-react/src/internal/form-fields.tsx create mode 100644 packages/@headlessui-react/src/internal/id.tsx create mode 100644 packages/@headlessui-react/src/internal/modal.tsx create mode 100644 packages/@headlessui-react/src/test-utils/scenarios.tsx create mode 100644 packages/@headlessui-react/src/utils/__snapshots__/render.test.tsx.snap delete mode 100644 packages/@headlessui-tailwindcss/jest.setup.js create mode 100644 packages/@headlessui-tailwindcss/src/__snapshots__/index.test.ts.snap create mode 100644 packages/@headlessui-vue/src/components/description/__snapshots__/description.test.ts.snap create mode 100644 packages/@headlessui-vue/src/components/label/__snapshots__/label.test.ts.snap create mode 100644 packages/@headlessui-vue/src/components/portal/__snapshots__/portal.test.ts.snap create mode 100644 packages/@headlessui-vue/src/components/transitions/__snapshots__/transition.test.ts.snap create mode 100644 scripts/make-nextjs-happy.js diff --git a/package.json b/package.json index 61c6b16..18e9b8a 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,9 @@ "workspaces": [ "packages/*" ], + "resolutions": { + "next/@swc/helpers": "0.4.36" + }, "scripts": { "react": "yarn workspace @headlessui/react", "react-playground": "yarn workspace playground-react dev", @@ -46,6 +49,7 @@ }, "devDependencies": { "@arethetypeswrong/cli": "^0.13.3", + "@swc-node/register": "^1.6.8", "@swc/core": "^1.2.131", "@swc/jest": "^0.2.17", "@testing-library/jest-dom": "^5.16.4", @@ -56,11 +60,11 @@ "jest": "26", "lint-staged": "^12.2.1", "npm-run-all": "^4.1.5", - "prettier": "^2.6.2", - "prettier-plugin-organize-imports": "^3.2.3", - "prettier-plugin-tailwindcss": "0.4", + "prettier": "^3.1.0", + "prettier-plugin-organize-imports": "^3.2.4", + "prettier-plugin-tailwindcss": "^0.5.7", "rimraf": "^3.0.2", "tslib": "^2.3.1", - "typescript": "^4.9.5" + "typescript": "^5.3.2" } } diff --git a/packages/@headlessui-react/CHANGELOG.md b/packages/@headlessui-react/CHANGELOG.md index 6df6ed8..955b95f 100644 --- a/packages/@headlessui-react/CHANGELOG.md +++ b/packages/@headlessui-react/CHANGELOG.md @@ -7,6 +7,29 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Add `immediate` prop to `` for immediately opening the Combobox when the `input` receives focus ([#2686](https://github.com/tailwindlabs/headlessui/pull/2686)) +- Add `virtual` prop to `Combobox` component ([#2779](https://github.com/tailwindlabs/headlessui/pull/2779)) +- Add new `Checkbox` component +- Add new `Radio` component as an alternative to the existing `RadioGroup.Option` component +- Add new `Button` component +- Add new `Input` component +- Add new `Textarea` component +- Add new `Select` component +- Add new `Field`, `Label`, `Description`, `Fieldset` and `Legend` components +- Add new `DataInteractive` component +- Add new `anchor` and `modal` prop to `ComboboxOptions`, `ListboxOptions`, `MenuItems` and `PopoverPanel` components +- Add new `ListboxSelectedOption` component +- Add new `MenuSection`, `MenuHeading`, and `MenuSeparator` components +- Add new simplified `data-*` attributes as an alternative to the existing `data-headlessui-state="..."` attribute +- Add `autoFocus` prop on focusable components (which maps to `data-autofocus`) + +### Changed + +- Bumped to React and React DOM 18 +- Dialog is focused by default instead of the first focusable element (unless an element exists with a `data-autofocus` in the dialog) + ### Fixed - Don't call ``'s `onClose` twice on mobile devices ([#2690](https://github.com/tailwindlabs/headlessui/pull/2690)) @@ -21,11 +44,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Fix outside click detection when component is mounted in the Shadow DOM ([#2866](https://github.com/tailwindlabs/headlessui/pull/2866)) - Fix CJS types ([#2880](https://github.com/tailwindlabs/headlessui/pull/2880)) - Fix error when transition classes contain new lines ([#2871](https://github.com/tailwindlabs/headlessui/pull/2871)) - -### Added - -- Add `immediate` prop to `` for immediately opening the Combobox when the `input` receives focus ([#2686](https://github.com/tailwindlabs/headlessui/pull/2686)) -- Add `virtual` prop to `Combobox` component ([#2779](https://github.com/tailwindlabs/headlessui/pull/2779)) +- Fix iOS scroll lock glitches ## [1.7.17] - 2023-08-17 diff --git a/packages/@headlessui-react/package.json b/packages/@headlessui-react/package.json index 11a37d6..e1f5e98 100644 --- a/packages/@headlessui-react/package.json +++ b/packages/@headlessui-react/package.json @@ -47,15 +47,17 @@ }, "devDependencies": { "@testing-library/react": "^13.0.0", - "@types/react": "^17.0.43", - "@types/react-dom": "^17.0.14", + "@types/react": "^18.2.14", + "@types/react-dom": "^18.2.6", "esbuild": "^0.11.18", - "react": "^18.0.0", - "react-dom": "^18.0.0", + "react": "^18.2.0", + "react-dom": "^18.2.0", "snapshot-diff": "^0.8.1" }, "dependencies": { - "@tanstack/react-virtual": "^3.0.0-beta.60", - "client-only": "^0.0.1" + "@floating-ui/react": "^0.26.2", + "@tanstack/react-virtual": "3.0.0-beta.60", + "@react-aria/focus": "^3.14.3", + "@react-aria/interactions": "3.0.0-nightly.2584" } } diff --git a/packages/@headlessui-react/src/components/button/button.test.tsx b/packages/@headlessui-react/src/components/button/button.test.tsx new file mode 100644 index 0000000..c7d6c96 --- /dev/null +++ b/packages/@headlessui-react/src/components/button/button.test.tsx @@ -0,0 +1,39 @@ +import { render, screen } from '@testing-library/react' +import React from 'react' +import { Button } from './button' + +describe('Rendering', () => { + describe('Button', () => { + it('should render a button', async () => { + render() + + expect(screen.getByRole('button')).toBeInTheDocument() + }) + + it('should default to `type="button"`', async () => { + render() + + expect(screen.getByRole('button')).toHaveAttribute('type', 'button') + }) + + it('should render a button using a render prop', () => { + render() + + expect(screen.getByRole('button').textContent).toEqual( + JSON.stringify({ + disabled: false, + hover: false, + focus: false, + active: false, + autofocus: false, + }) + ) + }) + + it('should map the `autoFocus` prop to a `data-autofocus` attribute', () => { + render() + + expect(screen.getByRole('button')).toHaveAttribute('data-autofocus') + }) + }) +}) diff --git a/packages/@headlessui-react/src/components/button/button.tsx b/packages/@headlessui-react/src/components/button/button.tsx new file mode 100644 index 0000000..031fef5 --- /dev/null +++ b/packages/@headlessui-react/src/components/button/button.tsx @@ -0,0 +1,88 @@ +'use client' + +import { useFocusRing } from '@react-aria/focus' +import { useHover } from '@react-aria/interactions' +import { useMemo, type ElementType, type Ref } from 'react' +import { useActivePress } from '../../hooks/use-active-press' +import { useDisabled } from '../../internal/disabled' +import type { Props } from '../../types' +import { + forwardRefWithAs, + mergeProps, + render, + type HasDisplayName, + type RefProp, +} from '../../utils/render' + +let DEFAULT_BUTTON_TAG = 'button' as const + +type ButtonRenderPropArg = { + disabled: boolean + hover: boolean + focus: boolean + active: boolean + autofocus: boolean +} +type ButtonPropsWeControl = never + +export type ButtonProps = Props< + TTag, + ButtonRenderPropArg, + ButtonPropsWeControl, + { + disabled?: boolean + autoFocus?: boolean + type?: 'button' | 'submit' | 'reset' + } +> + +function ButtonFn( + props: ButtonProps, + ref: Ref +) { + let providedDisabled = useDisabled() + let { disabled = providedDisabled || false, ...theirProps } = props + + let { isFocusVisible: focus, focusProps } = useFocusRing({ autoFocus: props.autoFocus ?? false }) + let { isHovered: hover, hoverProps } = useHover({ isDisabled: disabled }) + let { pressed: active, pressProps } = useActivePress({ disabled }) + + let ourProps = mergeProps( + { + ref, + disabled: disabled || undefined, + type: theirProps.type ?? 'button', + }, + focusProps, + hoverProps, + pressProps + ) + + let slot = useMemo( + () => + ({ + disabled, + hover, + focus, + active, + autofocus: props.autoFocus ?? false, + }) satisfies ButtonRenderPropArg, + [disabled, hover, focus, active, props.autoFocus] + ) + + return render({ + ourProps, + theirProps, + slot, + defaultTag: DEFAULT_BUTTON_TAG, + name: 'Button', + }) +} + +export interface _internal_ComponentButton extends HasDisplayName { + ( + props: ButtonProps & RefProp + ): JSX.Element +} + +export let Button = forwardRefWithAs(ButtonFn) as unknown as _internal_ComponentButton diff --git a/packages/@headlessui-react/src/components/checkbox/checkbox.test.tsx b/packages/@headlessui-react/src/components/checkbox/checkbox.test.tsx new file mode 100644 index 0000000..b8b2c46 --- /dev/null +++ b/packages/@headlessui-react/src/components/checkbox/checkbox.test.tsx @@ -0,0 +1,121 @@ +import { render } from '@testing-library/react' +import React, { useState } from 'react' +import { + CheckboxState, + assertCheckbox, + getCheckbox, +} from '../../test-utils/accessibility-assertions' +import { Keys, click, focus, press } from '../../test-utils/interactions' +import { + commonControlScenarios, + commonFormScenarios, + commonRenderingScenarios, +} from '../../test-utils/scenarios' +import { suppressConsoleLogs } from '../../test-utils/suppress-console-logs' +import { Checkbox, type CheckboxProps } from './checkbox' + +commonRenderingScenarios(Checkbox, { getElement: getCheckbox }) +commonControlScenarios(Checkbox) +commonFormScenarios((props) => , { + async performUserInteraction(control) { + await click(control) + }, +}) + +describe('Rendering', () => { + it( + 'should be possible to put the checkbox in an indeterminate state', + suppressConsoleLogs(async () => { + render() + + assertCheckbox({ state: CheckboxState.Indeterminate }) + }) + ) + + it( + 'should be possible to put the checkbox in an default checked state', + suppressConsoleLogs(async () => { + render() + + assertCheckbox({ state: CheckboxState.Checked }) + }) + ) + + it( + 'should render a checkbox in an unchecked state', + suppressConsoleLogs(async () => { + render() + + assertCheckbox({ state: CheckboxState.Unchecked }) + }) + ) +}) + +describe.each([ + [ + 'Uncontrolled', + function Example(props: CheckboxProps) { + return + }, + ], + [ + 'Controlled', + function Example(props: CheckboxProps) { + let [checked, setChecked] = useState(false) + return + }, + ], +])('Keyboard interactions (%s)', (_, Example) => { + describe('`Space` key', () => { + it( + 'should be possible to toggle a checkbox', + suppressConsoleLogs(async () => { + render() + + assertCheckbox({ state: CheckboxState.Unchecked }) + + await focus(getCheckbox()) + await press(Keys.Space) + + assertCheckbox({ state: CheckboxState.Checked }) + + await press(Keys.Space) + + assertCheckbox({ state: CheckboxState.Unchecked }) + }) + ) + }) +}) + +describe.each([ + [ + 'Uncontrolled', + function Example(props: CheckboxProps) { + return + }, + ], + [ + 'Controlled', + function Example(props: CheckboxProps) { + let [checked, setChecked] = useState(false) + return + }, + ], +])('Mouse interactions (%s)', (_, Example) => { + it( + 'should be possible to toggle a checkbox by clicking it', + suppressConsoleLogs(async () => { + render() + + assertCheckbox({ state: CheckboxState.Unchecked }) + + await click(getCheckbox()) + + assertCheckbox({ state: CheckboxState.Checked }) + + await click(getCheckbox()) + + assertCheckbox({ state: CheckboxState.Unchecked }) + }) + ) +}) diff --git a/packages/@headlessui-react/src/components/checkbox/checkbox.tsx b/packages/@headlessui-react/src/components/checkbox/checkbox.tsx new file mode 100644 index 0000000..ec1cbf0 --- /dev/null +++ b/packages/@headlessui-react/src/components/checkbox/checkbox.tsx @@ -0,0 +1,193 @@ +'use client' + +import { useFocusRing } from '@react-aria/focus' +import { useHover } from '@react-aria/interactions' +import React, { + useCallback, + useMemo, + useState, + type ElementType, + type KeyboardEvent as ReactKeyboardEvent, + type MouseEvent as ReactMouseEvent, + type Ref, +} from 'react' +import { useActivePress } from '../../hooks/use-active-press' +import { useControllable } from '../../hooks/use-controllable' +import { useDisposables } from '../../hooks/use-disposables' +import { useEvent } from '../../hooks/use-event' +import { useId } from '../../hooks/use-id' +import { useDisabled } from '../../internal/disabled' +import { FormFields } from '../../internal/form-fields' +import { useProvidedId } from '../../internal/id' +import type { Props } from '../../types' +import { isDisabledReactIssue7711 } from '../../utils/bugs' +import { + forwardRefWithAs, + mergeProps, + render, + type HasDisplayName, + type RefProp, +} from '../../utils/render' +import { useDescribedBy } from '../description/description' +import { Keys } from '../keyboard' +import { useLabelledBy } from '../label/label' + +let DEFAULT_CHECKBOX_TAG = 'span' as const +type CheckboxRenderPropArg = { + checked: boolean + changing: boolean + focus: boolean + active: boolean + hover: boolean + autofocus: boolean + disabled: boolean + indeterminate: boolean +} +type CheckboxPropsWeControl = + | 'aria-checked' + | 'aria-describedby' + | 'aria-disabled' + | 'aria-labelledby' + | 'role' + | 'tabIndex' + +export type CheckboxProps< + TTag extends ElementType = typeof DEFAULT_CHECKBOX_TAG, + TType = string, +> = Props< + TTag, + CheckboxRenderPropArg, + CheckboxPropsWeControl, + { + value?: TType + disabled?: boolean + indeterminate?: boolean + + checked?: boolean + defaultChecked?: boolean + autoFocus?: boolean + form?: string + name?: string + onChange?: (checked: boolean) => void + } +> + +function CheckboxFn( + props: CheckboxProps, + ref: Ref +) { + let internalId = useId() + let providedId = useProvidedId() + let providedDisabled = useDisabled() + let { + id = providedId || `headlessui-checkbox-${internalId}`, + disabled = providedDisabled || false, + checked: controlledChecked, + defaultChecked = false, + onChange: controlledOnChange, + name, + value, + form, + indeterminate = false, + ...theirProps + } = props + + let [checked, onChange] = useControllable(controlledChecked, controlledOnChange, defaultChecked) + + let labelledBy = useLabelledBy() + let describedBy = useDescribedBy() + + let d = useDisposables() + let [changing, setChanging] = useState(false) + let toggle = useEvent(() => { + setChanging(true) + onChange?.(!checked) + + d.nextFrame(() => { + setChanging(false) + }) + }) + + let handleClick = useEvent((event: ReactMouseEvent) => { + if (isDisabledReactIssue7711(event.currentTarget)) return event.preventDefault() + toggle() + }) + + let handleKeyDown = useEvent((event: ReactKeyboardEvent) => { + if (isDisabledReactIssue7711(event.currentTarget)) return event.preventDefault() + + switch (event.key) { + case Keys.Space: + event.preventDefault() + toggle() + break + } + }) + + let { isFocusVisible: focus, focusProps } = useFocusRing({ autoFocus: props.autoFocus ?? false }) + let { isHovered: hover, hoverProps } = useHover({ isDisabled: disabled ?? false }) + let { pressed: active, pressProps } = useActivePress({ disabled: disabled ?? false }) + + let ourProps = mergeProps( + { + ref, + id, + role: 'checkbox', + 'aria-checked': indeterminate ? 'mixed' : checked ? 'true' : 'false', + 'aria-labelledby': labelledBy, + 'aria-describedby': describedBy, + 'aria-disabled': disabled ? true : undefined, + indeterminate: indeterminate ? 'true' : undefined, + tabIndex: 0, + onKeyDown: disabled ? undefined : handleKeyDown, + onClick: disabled ? undefined : handleClick, + }, + focusProps, + hoverProps, + pressProps + ) + + let slot = useMemo( + () => + ({ + checked, + disabled, + hover, + focus, + active, + indeterminate, + changing, + autofocus: props.autoFocus ?? false, + }) satisfies CheckboxRenderPropArg, + [checked, indeterminate, disabled, hover, focus, active, changing, props.autoFocus] + ) + + let reset = useCallback(() => { + return onChange?.(defaultChecked) + }, [onChange /* Explicitly ignoring `defaultChecked` */]) + + return ( + <> + {name != null && ( + + )} + {render({ + ourProps, + theirProps, + slot, + defaultTag: DEFAULT_CHECKBOX_TAG, + name: 'Checkbox', + })} + + ) +} + +// --- + +export interface _internal_ComponentCheckbox extends HasDisplayName { + ( + props: CheckboxProps & RefProp + ): JSX.Element +} + +export let Checkbox = forwardRefWithAs(CheckboxFn) as unknown as _internal_ComponentCheckbox diff --git a/packages/@headlessui-react/src/components/combobox-button/combobox-button.tsx b/packages/@headlessui-react/src/components/combobox-button/combobox-button.tsx new file mode 100644 index 0000000..5c9d120 --- /dev/null +++ b/packages/@headlessui-react/src/components/combobox-button/combobox-button.tsx @@ -0,0 +1,3 @@ +// Next.js barrel file improvements (GENERATED FILE) +export type * from '../combobox/combobox' +export { ComboboxButton } from '../combobox/combobox' diff --git a/packages/@headlessui-react/src/components/combobox-input/combobox-input.tsx b/packages/@headlessui-react/src/components/combobox-input/combobox-input.tsx new file mode 100644 index 0000000..d2ca6f9 --- /dev/null +++ b/packages/@headlessui-react/src/components/combobox-input/combobox-input.tsx @@ -0,0 +1,3 @@ +// Next.js barrel file improvements (GENERATED FILE) +export type * from '../combobox/combobox' +export { ComboboxInput } from '../combobox/combobox' diff --git a/packages/@headlessui-react/src/components/combobox-label/combobox-label.tsx b/packages/@headlessui-react/src/components/combobox-label/combobox-label.tsx new file mode 100644 index 0000000..452189e --- /dev/null +++ b/packages/@headlessui-react/src/components/combobox-label/combobox-label.tsx @@ -0,0 +1,3 @@ +// Next.js barrel file improvements (GENERATED FILE) +export type * from '../combobox/combobox' +export { ComboboxLabel } from '../combobox/combobox' diff --git a/packages/@headlessui-react/src/components/combobox-option/combobox-option.tsx b/packages/@headlessui-react/src/components/combobox-option/combobox-option.tsx new file mode 100644 index 0000000..33e774d --- /dev/null +++ b/packages/@headlessui-react/src/components/combobox-option/combobox-option.tsx @@ -0,0 +1,3 @@ +// Next.js barrel file improvements (GENERATED FILE) +export type * from '../combobox/combobox' +export { ComboboxOption } from '../combobox/combobox' diff --git a/packages/@headlessui-react/src/components/combobox-options/combobox-options.tsx b/packages/@headlessui-react/src/components/combobox-options/combobox-options.tsx new file mode 100644 index 0000000..7044ed5 --- /dev/null +++ b/packages/@headlessui-react/src/components/combobox-options/combobox-options.tsx @@ -0,0 +1,3 @@ +// Next.js barrel file improvements (GENERATED FILE) +export type * from '../combobox/combobox' +export { ComboboxOptions } from '../combobox/combobox' diff --git a/packages/@headlessui-react/src/components/combobox/combobox.test.tsx b/packages/@headlessui-react/src/components/combobox/combobox.test.tsx index 7a9bb90..525f345 100644 --- a/packages/@headlessui-react/src/components/combobox/combobox.test.tsx +++ b/packages/@headlessui-react/src/components/combobox/combobox.test.tsx @@ -1,6 +1,8 @@ import { render } from '@testing-library/react' import React, { createElement, useEffect, useState } from 'react' import { + ComboboxMode, + ComboboxState, assertActiveComboboxOption, assertActiveElement, assertCombobox, @@ -15,23 +17,21 @@ import { assertNoActiveComboboxOption, assertNoSelectedComboboxOption, assertNotActiveComboboxOption, - ComboboxMode, - ComboboxState, getByText, getComboboxButton, getComboboxButtons, - getComboboxes, getComboboxInput, getComboboxInputs, getComboboxLabel, getComboboxOptions, + getComboboxes, } from '../../test-utils/accessibility-assertions' import { + Keys, + MouseButton, blur, click, focus, - Keys, - MouseButton, mouseLeave, mouseMove, press, @@ -41,7 +41,7 @@ import { word, } from '../../test-utils/interactions' import { mockingConsoleLogs, suppressConsoleLogs } from '../../test-utils/suppress-console-logs' -import { Transition } from '../transitions/transition' +import { Transition } from '../transition/transition' import { Combobox } from './combobox' let NOOP = () => {} @@ -70,10 +70,17 @@ describe('safeguards', () => { ])( 'should error when we are using a <%s /> without a parent ', suppressConsoleLogs((name, Component) => { - // @ts-expect-error This is fine - expect(() => render(createElement(Component))).toThrowError( - `<${name} /> is missing a parent component.` - ) + if (name === 'Combobox.Label') { + // @ts-expect-error This is fine + expect(() => render(createElement(Component))).toThrow( + 'You used a