diff --git a/package.json b/package.json
index dfbb973..249cfff 100644
--- a/package.json
+++ b/package.json
@@ -34,11 +34,11 @@
"devDependencies": {
"@tailwindcss/ui": "^0.6.2",
"@testing-library/jest-dom": "^5.11.4",
- "@types/node": "^14.11.8",
+ "@types/node": "^14.11.10",
"husky": "^4.3.0",
- "lint-staged": "^10.4.0",
+ "lint-staged": "^10.4.2",
"prismjs": "^1.22.0",
- "tailwindcss": "^1.9.1",
+ "tailwindcss": "^1.9.4",
"tsdx": "^0.14.1",
"tslib": "^2.0.3",
"typescript": "^3.9.7"
diff --git a/packages/@headlessui-react/README.md b/packages/@headlessui-react/README.md
index 2f6722a..ef63d79 100644
--- a/packages/@headlessui-react/README.md
+++ b/packages/@headlessui-react/README.md
@@ -317,6 +317,7 @@ function MyComponent({ isShowing }) {
| `show` | Boolean | Whether the children should be shown or hidden. |
| `as` | String Component _(Default: `'div'`)_ | The element or component to render in place of the `Transition` itself. |
| `appear` | Boolean _(Default: `false`)_ | Whether the transition should run on initial mount. |
+| `unmount` | Boolean _(Default: `true`)_ | Whether the element should be unmounted or hidden based on the show state. |
| `enter` | String _(Default: '')_ | Classes to add to the transitioning element during the entire enter phase. |
| `enterFrom` | String _(Default: '')_ | Classes to add to the transitioning element before the enter phase starts. |
| `enterTo` | String _(Default: '')_ | Classes to add to the transitioning element immediately after the enter phase starts. |
@@ -358,6 +359,7 @@ function MyComponent({ isShowing }) {
| ----------- | ------------------------------------- | ------------------------------------------------------------------------------------- |
| `as` | String Component _(Default: `'div'`)_ | The element or component to render in place of the `Transition.Child` itself. |
| `appear` | Boolean _(Default: `false`)_ | Whether the transition should run on initial mount. |
+| `unmount` | Boolean _(Default: `true`)_ | Whether the element should be unmounted or hidden based on the show state. |
| `enter` | String _(Default: '')_ | Classes to add to the transitioning element during the entire enter phase. |
| `enterFrom` | String _(Default: '')_ | Classes to add to the transitioning element before the enter phase starts. |
| `enterTo` | String _(Default: '')_ | Classes to add to the transitioning element immediately after the enter phase starts. |
@@ -726,10 +728,13 @@ function MyDropdown() {
##### Props
-| Prop | Type | Default | Description |
-| -------- | ------------------- | ------- | --------------------------------------------------------------------------- |
-| `as` | String \| Component | `div` | The element or component the `Menu.Items` should render as. |
-| `static` | Boolean | `false` | Whether the element should ignore the internally managed open/closed state. |
+| Prop | Type | Default | Description |
+| --------- | ------------------- | ------- | --------------------------------------------------------------------------------- |
+| `as` | String \| Component | `div` | The element or component the `Menu.Items` 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
@@ -1231,10 +1236,13 @@ function MyListbox() {
##### Props
-| Prop | Type | Default | Description |
-| -------- | ------------------- | ------- | --------------------------------------------------------------------------- |
-| `as` | String \| Component | `ul` | The element or component the `Listbox.Options` should render as. |
-| `static` | Boolean | `false` | Whether the element should ignore the internally managed open/closed state. |
+| Prop | Type | Default | Description |
+| --------- | ------------------- | ------- | --------------------------------------------------------------------------------- |
+| `as` | String \| Component | `ul` | The element or component the `Listbox.Options` 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
diff --git a/packages/@headlessui-react/package.json b/packages/@headlessui-react/package.json
index 90b9987..29ab3ea 100644
--- a/packages/@headlessui-react/package.json
+++ b/packages/@headlessui-react/package.json
@@ -31,14 +31,14 @@
"react": ">=16"
},
"devDependencies": {
- "@types/react": "^16.9.52",
+ "@types/react": "^16.9.53",
"@types/react-dom": "^16.9.8",
"@popperjs/core": "^2.5.3",
- "@testing-library/react": "^11.0.4",
+ "@testing-library/react": "^11.1.0",
"framer-motion": "^2.9.1",
"next": "9.5.5",
- "react": "^16.13.1",
- "react-dom": "^16.13.1",
+ "react": "^16.14.0",
+ "react-dom": "^16.14.0",
"snapshot-diff": "^0.8.1"
}
}
diff --git a/packages/@headlessui-react/pages/transitions/component-examples/nested/hidden.tsx b/packages/@headlessui-react/pages/transitions/component-examples/nested/hidden.tsx
new file mode 100644
index 0000000..7cec0cc
--- /dev/null
+++ b/packages/@headlessui-react/pages/transitions/component-examples/nested/hidden.tsx
@@ -0,0 +1,60 @@
+import React, { useState } from 'react'
+import { Transition } from '@headlessui/react'
+
+export default function Home() {
+ const [isOpen, setIsOpen] = useState(true)
+
+ return (
+ <>
+
+
+
+ setIsOpen(v => !v)}
+ className="inline-flex items-center px-3 py-2 text-sm font-medium leading-4 text-gray-700 transition bg-white border border-gray-300 rounded-md duration-150-out hover:text-gray-500 focus:outline-none focus:border-blue-300 focus:shadow-outline-blue active:text-gray-800 active:bg-gray-50"
+ >
+ {isOpen ? 'Hide' : 'Show'}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ >
+ )
+}
+
+function Box({ children }: { children?: React.ReactNode }) {
+ return (
+
+
+ This is a box
+ {children}
+
+
+ )
+}
diff --git a/packages/@headlessui-react/pages/transitions/component-examples/nested/unmount.tsx b/packages/@headlessui-react/pages/transitions/component-examples/nested/unmount.tsx
new file mode 100644
index 0000000..4751229
--- /dev/null
+++ b/packages/@headlessui-react/pages/transitions/component-examples/nested/unmount.tsx
@@ -0,0 +1,60 @@
+import React, { useState } from 'react'
+import { Transition } from '@headlessui/react'
+
+export default function Home() {
+ const [isOpen, setIsOpen] = useState(true)
+
+ return (
+ <>
+
+
+
+ setIsOpen(v => !v)}
+ className="inline-flex items-center px-3 py-2 text-sm font-medium leading-4 text-gray-700 transition bg-white border border-gray-300 rounded-md duration-150-out hover:text-gray-500 focus:outline-none focus:border-blue-300 focus:shadow-outline-blue active:text-gray-800 active:bg-gray-50"
+ >
+ {isOpen ? 'Hide' : 'Show'}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ >
+ )
+}
+
+function Box({ children }: { children?: React.ReactNode }) {
+ return (
+
+
+ This is a box
+ {children}
+
+
+ )
+}
diff --git a/packages/@headlessui-react/pages/transitions/component-examples/peek-a-boo.tsx b/packages/@headlessui-react/pages/transitions/component-examples/peek-a-boo.tsx
new file mode 100644
index 0000000..9318423
--- /dev/null
+++ b/packages/@headlessui-react/pages/transitions/component-examples/peek-a-boo.tsx
@@ -0,0 +1,38 @@
+import React, { useState } from 'react'
+import { Transition } from '@headlessui/react'
+
+export default function Home() {
+ const [isOpen, setIsOpen] = useState(true)
+
+ return (
+ <>
+
+
+
+ setIsOpen(v => !v)}
+ className="inline-flex items-center px-3 py-2 text-sm font-medium leading-4 text-gray-700 transition duration-150 ease-in-out bg-white border border-gray-300 rounded-md hover:text-gray-500 focus:outline-none focus:border-blue-300 focus:shadow-outline-blue active:text-gray-800 active:bg-gray-50"
+ >
+ {isOpen ? 'Hide' : 'Show'}
+
+
+
+
+ Contents to show and hide
+
+
+
+ >
+ )
+}
diff --git a/packages/@headlessui-react/pages/transitions/full-page-examples/layout-with-sidebar.tsx b/packages/@headlessui-react/pages/transitions/full-page-examples/layout-with-sidebar.tsx
index b0dfcaf..f6b3129 100644
--- a/packages/@headlessui-react/pages/transitions/full-page-examples/layout-with-sidebar.tsx
+++ b/packages/@headlessui-react/pages/transitions/full-page-examples/layout-with-sidebar.tsx
@@ -26,9 +26,10 @@ export default function App() {
{/* Off-canvas menu for mobile */}
-
+
{/* Off-canvas menu overlay, show/hide based on off-canvas menu state. */}
- {ref => (
-
+ {() => (
+
setMobileOpen(false)}
className="absolute inset-0 opacity-75 bg-cool-gray-600"
@@ -48,6 +49,7 @@ export default function App() {
{/* Off-canvas menu, show/hide based on off-canvas menu state. */}
{
)
assertListboxButton({
- state: ListboxState.Closed,
+ state: ListboxState.InvisibleUnmounted,
attributes: { id: 'headlessui-listbox-button-1' },
})
- assertListbox({ state: ListboxState.Closed })
+ assertListbox({ state: ListboxState.InvisibleUnmounted })
})
)
})
@@ -105,18 +105,18 @@ describe('Rendering', () => {
)
assertListboxButton({
- state: ListboxState.Closed,
+ state: ListboxState.InvisibleUnmounted,
attributes: { id: 'headlessui-listbox-button-1' },
})
- assertListbox({ state: ListboxState.Closed })
+ assertListbox({ state: ListboxState.InvisibleUnmounted })
await click(getListboxButton())
assertListboxButton({
- state: ListboxState.Open,
+ state: ListboxState.Visible,
attributes: { id: 'headlessui-listbox-button-1' },
})
- assertListbox({ state: ListboxState.Open })
+ assertListbox({ state: ListboxState.Visible })
})
)
})
@@ -138,14 +138,14 @@ describe('Rendering', () => {
)
assertListboxButton({
- state: ListboxState.Closed,
+ state: ListboxState.InvisibleUnmounted,
attributes: { id: 'headlessui-listbox-button-2' },
})
assertListboxLabel({
attributes: { id: 'headlessui-listbox-label-1' },
textContent: JSON.stringify({ open: false }),
})
- assertListbox({ state: ListboxState.Closed })
+ assertListbox({ state: ListboxState.InvisibleUnmounted })
await click(getListboxButton())
@@ -153,7 +153,7 @@ describe('Rendering', () => {
attributes: { id: 'headlessui-listbox-label-1' },
textContent: JSON.stringify({ open: true }),
})
- assertListbox({ state: ListboxState.Open })
+ assertListbox({ state: ListboxState.Visible })
assertListboxLabelLinkedWithListbox()
assertListboxButtonLinkedWithListboxLabel()
})
@@ -179,7 +179,7 @@ describe('Rendering', () => {
textContent: JSON.stringify({ open: false }),
tag: 'p',
})
- assertListbox({ state: ListboxState.Closed })
+ assertListbox({ state: ListboxState.InvisibleUnmounted })
await click(getListboxButton())
assertListboxLabel({
@@ -187,7 +187,7 @@ describe('Rendering', () => {
textContent: JSON.stringify({ open: true }),
tag: 'p',
})
- assertListbox({ state: ListboxState.Open })
+ assertListbox({ state: ListboxState.Visible })
})
)
})
@@ -208,20 +208,20 @@ describe('Rendering', () => {
)
assertListboxButton({
- state: ListboxState.Closed,
+ state: ListboxState.InvisibleUnmounted,
attributes: { id: 'headlessui-listbox-button-1' },
textContent: JSON.stringify({ open: false, focused: false }),
})
- assertListbox({ state: ListboxState.Closed })
+ assertListbox({ state: ListboxState.InvisibleUnmounted })
await click(getListboxButton())
assertListboxButton({
- state: ListboxState.Open,
+ state: ListboxState.Visible,
attributes: { id: 'headlessui-listbox-button-1' },
textContent: JSON.stringify({ open: true, focused: false }),
})
- assertListbox({ state: ListboxState.Open })
+ assertListbox({ state: ListboxState.Visible })
})
)
@@ -242,20 +242,20 @@ describe('Rendering', () => {
)
assertListboxButton({
- state: ListboxState.Closed,
+ state: ListboxState.InvisibleUnmounted,
attributes: { id: 'headlessui-listbox-button-1' },
textContent: JSON.stringify({ open: false, focused: false }),
})
- assertListbox({ state: ListboxState.Closed })
+ assertListbox({ state: ListboxState.InvisibleUnmounted })
await click(getListboxButton())
assertListboxButton({
- state: ListboxState.Open,
+ state: ListboxState.Visible,
attributes: { id: 'headlessui-listbox-button-1' },
textContent: JSON.stringify({ open: true, focused: false }),
})
- assertListbox({ state: ListboxState.Open })
+ assertListbox({ state: ListboxState.Visible })
})
)
@@ -278,10 +278,10 @@ describe('Rendering', () => {
// await new Promise(requestAnimationFrame)
assertListboxButton({
- state: ListboxState.Closed,
+ state: ListboxState.InvisibleUnmounted,
attributes: { id: 'headlessui-listbox-button-2' },
})
- assertListbox({ state: ListboxState.Closed })
+ assertListbox({ state: ListboxState.InvisibleUnmounted })
assertListboxButtonLinkedWithListboxLabel()
})
)
@@ -305,19 +305,19 @@ describe('Rendering', () => {
)
assertListboxButton({
- state: ListboxState.Closed,
+ state: ListboxState.InvisibleUnmounted,
attributes: { id: 'headlessui-listbox-button-1' },
})
- assertListbox({ state: ListboxState.Closed })
+ assertListbox({ state: ListboxState.InvisibleUnmounted })
await click(getListboxButton())
assertListboxButton({
- state: ListboxState.Open,
+ state: ListboxState.Visible,
attributes: { id: 'headlessui-listbox-button-1' },
})
assertListbox({
- state: ListboxState.Open,
+ state: ListboxState.Visible,
textContent: JSON.stringify({ open: true }),
})
assertActiveElement(getListbox())
@@ -339,6 +339,26 @@ describe('Rendering', () => {
// Let's verify that the Listbox is already there
expect(getListbox()).not.toBe(null)
})
+
+ it('should be possible to use a different render strategy for the Listbox.Options', async () => {
+ render(
+
+ Trigger
+
+ Option A
+ Option B
+ Option C
+
+
+ )
+
+ assertListbox({ state: ListboxState.InvisibleHidden })
+
+ // Let's open the Listbox, to see if it is not hidden anymore
+ await click(getListboxButton())
+
+ assertListbox({ state: ListboxState.Visible })
+ })
})
describe('Listbox.Option', () => {
@@ -355,19 +375,19 @@ describe('Rendering', () => {
)
assertListboxButton({
- state: ListboxState.Closed,
+ state: ListboxState.InvisibleUnmounted,
attributes: { id: 'headlessui-listbox-button-1' },
})
- assertListbox({ state: ListboxState.Closed })
+ assertListbox({ state: ListboxState.InvisibleUnmounted })
await click(getListboxButton())
assertListboxButton({
- state: ListboxState.Open,
+ state: ListboxState.Visible,
attributes: { id: 'headlessui-listbox-button-1' },
})
assertListbox({
- state: ListboxState.Open,
+ state: ListboxState.Visible,
textContent: JSON.stringify({ active: false, selected: false, disabled: false }),
})
})
@@ -397,10 +417,10 @@ describe('Rendering composition', () => {
)
assertListboxButton({
- state: ListboxState.Closed,
+ state: ListboxState.InvisibleUnmounted,
attributes: { id: 'headlessui-listbox-button-1' },
})
- assertListbox({ state: ListboxState.Closed })
+ assertListbox({ state: ListboxState.InvisibleUnmounted })
// Open Listbox
await click(getListboxButton())
@@ -472,10 +492,10 @@ describe('Rendering composition', () => {
)
assertListboxButton({
- state: ListboxState.Closed,
+ state: ListboxState.InvisibleUnmounted,
attributes: { id: 'headlessui-listbox-button-1' },
})
- assertListbox({ state: ListboxState.Closed })
+ assertListbox({ state: ListboxState.InvisibleUnmounted })
// Open Listbox
await click(getListboxButton())
@@ -503,10 +523,10 @@ describe('Keyboard interactions', () => {
)
assertListboxButton({
- state: ListboxState.Closed,
+ state: ListboxState.InvisibleUnmounted,
attributes: { id: 'headlessui-listbox-button-1' },
})
- assertListbox({ state: ListboxState.Closed })
+ assertListbox({ state: ListboxState.InvisibleUnmounted })
// Focus the button
getListboxButton()?.focus()
@@ -514,10 +534,10 @@ describe('Keyboard interactions', () => {
// Open listbox
await press(Keys.Enter)
- // Verify it is open
- assertListboxButton({ state: ListboxState.Open })
+ // Verify it is visible
+ assertListboxButton({ state: ListboxState.Visible })
assertListbox({
- state: ListboxState.Open,
+ state: ListboxState.Visible,
attributes: { id: 'headlessui-listbox-options-2' },
})
assertActiveElement(getListbox())
@@ -549,10 +569,10 @@ describe('Keyboard interactions', () => {
)
assertListboxButton({
- state: ListboxState.Closed,
+ state: ListboxState.InvisibleUnmounted,
attributes: { id: 'headlessui-listbox-button-1' },
})
- assertListbox({ state: ListboxState.Closed })
+ assertListbox({ state: ListboxState.InvisibleUnmounted })
// Focus the button
getListboxButton()?.focus()
@@ -562,10 +582,10 @@ describe('Keyboard interactions', () => {
// Verify it is still closed
assertListboxButton({
- state: ListboxState.Closed,
+ state: ListboxState.InvisibleUnmounted,
attributes: { id: 'headlessui-listbox-button-1' },
})
- assertListbox({ state: ListboxState.Closed })
+ assertListbox({ state: ListboxState.InvisibleUnmounted })
})
)
@@ -584,10 +604,10 @@ describe('Keyboard interactions', () => {
)
assertListboxButton({
- state: ListboxState.Closed,
+ state: ListboxState.InvisibleUnmounted,
attributes: { id: 'headlessui-listbox-button-1' },
})
- assertListbox({ state: ListboxState.Closed })
+ assertListbox({ state: ListboxState.InvisibleUnmounted })
// Focus the button
getListboxButton()?.focus()
@@ -595,10 +615,10 @@ describe('Keyboard interactions', () => {
// Open listbox
await press(Keys.Enter)
- // Verify it is open
- assertListboxButton({ state: ListboxState.Open })
+ // Verify it is visible
+ assertListboxButton({ state: ListboxState.Visible })
assertListbox({
- state: ListboxState.Open,
+ state: ListboxState.Visible,
attributes: { id: 'headlessui-listbox-options-2' },
})
assertActiveElement(getListbox())
@@ -614,6 +634,67 @@ describe('Keyboard interactions', () => {
})
)
+ it(
+ 'should be possible to open the listbox with Enter, and focus the selected option (when using the `hidden` render strategy)',
+ suppressConsoleLogs(async () => {
+ render(
+
+ Trigger
+
+ Option A
+ Option B
+ Option C
+
+
+ )
+
+ assertListboxButton({
+ state: ListboxState.InvisibleHidden,
+ attributes: { id: 'headlessui-listbox-button-1' },
+ })
+ assertListbox({ state: ListboxState.InvisibleHidden })
+
+ // Focus the button
+ getListboxButton()?.focus()
+
+ // Open listbox
+ await press(Keys.Enter)
+
+ // Verify it is visible
+ assertListboxButton({ state: ListboxState.Visible })
+ assertListbox({
+ state: ListboxState.Visible,
+ attributes: { id: 'headlessui-listbox-options-2' },
+ })
+ assertActiveElement(getListbox())
+ assertListboxButtonLinkedWithListbox()
+
+ const options = getListboxOptions()
+
+ // Hover over Option A
+ await mouseMove(options[0])
+
+ // Verify that Option A is active
+ assertActiveListboxOption(options[0])
+
+ // Verify that Option B is still selected
+ assertListboxOption(options[1], { selected: true })
+
+ // Close/Hide the listbox
+ await press(Keys.Escape)
+
+ // Re-open the listbox
+ await click(getListboxButton())
+
+ // Verify we have listbox options
+ expect(options).toHaveLength(3)
+ options.forEach((option, i) => assertListboxOption(option, { selected: i === 1 }))
+
+ // Verify that the second listbox option is active (because it is already selected)
+ assertActiveListboxOption(options[1])
+ })
+ )
+
it(
'should be possible to open the listbox with Enter, and focus the selected option (with a list of objects)',
suppressConsoleLogs(async () => {
@@ -637,10 +718,10 @@ describe('Keyboard interactions', () => {
)
assertListboxButton({
- state: ListboxState.Closed,
+ state: ListboxState.InvisibleUnmounted,
attributes: { id: 'headlessui-listbox-button-1' },
})
- assertListbox({ state: ListboxState.Closed })
+ assertListbox({ state: ListboxState.InvisibleUnmounted })
// Focus the button
getListboxButton()?.focus()
@@ -648,10 +729,10 @@ describe('Keyboard interactions', () => {
// Open listbox
await press(Keys.Enter)
- // Verify it is open
- assertListboxButton({ state: ListboxState.Open })
+ // Verify it is visible
+ assertListboxButton({ state: ListboxState.Visible })
assertListbox({
- state: ListboxState.Open,
+ state: ListboxState.Visible,
attributes: { id: 'headlessui-listbox-options-2' },
})
assertActiveElement(getListbox())
@@ -677,14 +758,14 @@ describe('Keyboard interactions', () => {
)
- assertListbox({ state: ListboxState.Closed })
+ assertListbox({ state: ListboxState.InvisibleUnmounted })
// Focus the button
getListboxButton()?.focus()
// Open listbox
await press(Keys.Enter)
- assertListbox({ state: ListboxState.Open })
+ assertListbox({ state: ListboxState.Visible })
assertActiveElement(getListbox())
assertNoActiveListboxOption()
@@ -708,10 +789,10 @@ describe('Keyboard interactions', () => {
)
assertListboxButton({
- state: ListboxState.Closed,
+ state: ListboxState.InvisibleUnmounted,
attributes: { id: 'headlessui-listbox-button-1' },
})
- assertListbox({ state: ListboxState.Closed })
+ assertListbox({ state: ListboxState.InvisibleUnmounted })
// Focus the button
getListboxButton()?.focus()
@@ -745,10 +826,10 @@ describe('Keyboard interactions', () => {
)
assertListboxButton({
- state: ListboxState.Closed,
+ state: ListboxState.InvisibleUnmounted,
attributes: { id: 'headlessui-listbox-button-1' },
})
- assertListbox({ state: ListboxState.Closed })
+ assertListbox({ state: ListboxState.InvisibleUnmounted })
// Focus the button
getListboxButton()?.focus()
@@ -784,10 +865,10 @@ describe('Keyboard interactions', () => {
)
assertListboxButton({
- state: ListboxState.Closed,
+ state: ListboxState.InvisibleUnmounted,
attributes: { id: 'headlessui-listbox-button-1' },
})
- assertListbox({ state: ListboxState.Closed })
+ assertListbox({ state: ListboxState.InvisibleUnmounted })
// Focus the button
getListboxButton()?.focus()
@@ -814,23 +895,23 @@ describe('Keyboard interactions', () => {
)
assertListboxButton({
- state: ListboxState.Closed,
+ state: ListboxState.InvisibleUnmounted,
attributes: { id: 'headlessui-listbox-button-1' },
})
- assertListbox({ state: ListboxState.Closed })
+ assertListbox({ state: ListboxState.InvisibleUnmounted })
// Open listbox
await click(getListboxButton())
- // Verify it is open
- assertListboxButton({ state: ListboxState.Open })
+ // Verify it is visible
+ assertListboxButton({ state: ListboxState.Visible })
// Close listbox
await press(Keys.Enter)
// Verify it is closed
- assertListboxButton({ state: ListboxState.Closed })
- assertListbox({ state: ListboxState.Closed })
+ assertListboxButton({ state: ListboxState.InvisibleUnmounted })
+ assertListbox({ state: ListboxState.InvisibleUnmounted })
// Verify the button is focused again
assertActiveElement(getListboxButton())
@@ -866,16 +947,16 @@ describe('Keyboard interactions', () => {
render( )
assertListboxButton({
- state: ListboxState.Closed,
+ state: ListboxState.InvisibleUnmounted,
attributes: { id: 'headlessui-listbox-button-1' },
})
- assertListbox({ state: ListboxState.Closed })
+ assertListbox({ state: ListboxState.InvisibleUnmounted })
// Open listbox
await click(getListboxButton())
- // Verify it is open
- assertListboxButton({ state: ListboxState.Open })
+ // Verify it is visible
+ assertListboxButton({ state: ListboxState.Visible })
// Activate the first listbox option
const options = getListboxOptions()
@@ -885,8 +966,8 @@ describe('Keyboard interactions', () => {
await press(Keys.Enter)
// Verify it is closed
- assertListboxButton({ state: ListboxState.Closed })
- assertListbox({ state: ListboxState.Closed })
+ assertListboxButton({ state: ListboxState.InvisibleUnmounted })
+ assertListbox({ state: ListboxState.InvisibleUnmounted })
// Verify we got the change event
expect(handleChange).toHaveBeenCalledTimes(1)
@@ -920,10 +1001,10 @@ describe('Keyboard interactions', () => {
)
assertListboxButton({
- state: ListboxState.Closed,
+ state: ListboxState.InvisibleUnmounted,
attributes: { id: 'headlessui-listbox-button-1' },
})
- assertListbox({ state: ListboxState.Closed })
+ assertListbox({ state: ListboxState.InvisibleUnmounted })
// Focus the button
getListboxButton()?.focus()
@@ -931,10 +1012,10 @@ describe('Keyboard interactions', () => {
// Open listbox
await press(Keys.Space)
- // Verify it is open
- assertListboxButton({ state: ListboxState.Open })
+ // Verify it is visible
+ assertListboxButton({ state: ListboxState.Visible })
assertListbox({
- state: ListboxState.Open,
+ state: ListboxState.Visible,
attributes: { id: 'headlessui-listbox-options-2' },
})
assertActiveElement(getListbox())
@@ -963,10 +1044,10 @@ describe('Keyboard interactions', () => {
)
assertListboxButton({
- state: ListboxState.Closed,
+ state: ListboxState.InvisibleUnmounted,
attributes: { id: 'headlessui-listbox-button-1' },
})
- assertListbox({ state: ListboxState.Closed })
+ assertListbox({ state: ListboxState.InvisibleUnmounted })
// Focus the button
getListboxButton()?.focus()
@@ -976,10 +1057,10 @@ describe('Keyboard interactions', () => {
// Verify it is still closed
assertListboxButton({
- state: ListboxState.Closed,
+ state: ListboxState.InvisibleUnmounted,
attributes: { id: 'headlessui-listbox-button-1' },
})
- assertListbox({ state: ListboxState.Closed })
+ assertListbox({ state: ListboxState.InvisibleUnmounted })
})
)
@@ -998,10 +1079,10 @@ describe('Keyboard interactions', () => {
)
assertListboxButton({
- state: ListboxState.Closed,
+ state: ListboxState.InvisibleUnmounted,
attributes: { id: 'headlessui-listbox-button-1' },
})
- assertListbox({ state: ListboxState.Closed })
+ assertListbox({ state: ListboxState.InvisibleUnmounted })
// Focus the button
getListboxButton()?.focus()
@@ -1009,10 +1090,10 @@ describe('Keyboard interactions', () => {
// Open listbox
await press(Keys.Space)
- // Verify it is open
- assertListboxButton({ state: ListboxState.Open })
+ // Verify it is visible
+ assertListboxButton({ state: ListboxState.Visible })
assertListbox({
- state: ListboxState.Open,
+ state: ListboxState.Visible,
attributes: { id: 'headlessui-listbox-options-2' },
})
assertActiveElement(getListbox())
@@ -1038,14 +1119,14 @@ describe('Keyboard interactions', () => {
)
- assertListbox({ state: ListboxState.Closed })
+ assertListbox({ state: ListboxState.InvisibleUnmounted })
// Focus the button
getListboxButton()?.focus()
// Open listbox
await press(Keys.Space)
- assertListbox({ state: ListboxState.Open })
+ assertListbox({ state: ListboxState.Visible })
assertActiveElement(getListbox())
assertNoActiveListboxOption()
@@ -1069,10 +1150,10 @@ describe('Keyboard interactions', () => {
)
assertListboxButton({
- state: ListboxState.Closed,
+ state: ListboxState.InvisibleUnmounted,
attributes: { id: 'headlessui-listbox-button-1' },
})
- assertListbox({ state: ListboxState.Closed })
+ assertListbox({ state: ListboxState.InvisibleUnmounted })
// Focus the button
getListboxButton()?.focus()
@@ -1106,10 +1187,10 @@ describe('Keyboard interactions', () => {
)
assertListboxButton({
- state: ListboxState.Closed,
+ state: ListboxState.InvisibleUnmounted,
attributes: { id: 'headlessui-listbox-button-1' },
})
- assertListbox({ state: ListboxState.Closed })
+ assertListbox({ state: ListboxState.InvisibleUnmounted })
// Focus the button
getListboxButton()?.focus()
@@ -1145,10 +1226,10 @@ describe('Keyboard interactions', () => {
)
assertListboxButton({
- state: ListboxState.Closed,
+ state: ListboxState.InvisibleUnmounted,
attributes: { id: 'headlessui-listbox-button-1' },
})
- assertListbox({ state: ListboxState.Closed })
+ assertListbox({ state: ListboxState.InvisibleUnmounted })
// Focus the button
getListboxButton()?.focus()
@@ -1189,16 +1270,16 @@ describe('Keyboard interactions', () => {
render( )
assertListboxButton({
- state: ListboxState.Closed,
+ state: ListboxState.InvisibleUnmounted,
attributes: { id: 'headlessui-listbox-button-1' },
})
- assertListbox({ state: ListboxState.Closed })
+ assertListbox({ state: ListboxState.InvisibleUnmounted })
// Open listbox
await click(getListboxButton())
- // Verify it is open
- assertListboxButton({ state: ListboxState.Open })
+ // Verify it is visible
+ assertListboxButton({ state: ListboxState.Visible })
// Activate the first listbox option
const options = getListboxOptions()
@@ -1208,8 +1289,8 @@ describe('Keyboard interactions', () => {
await press(Keys.Space)
// Verify it is closed
- assertListboxButton({ state: ListboxState.Closed })
- assertListbox({ state: ListboxState.Closed })
+ assertListboxButton({ state: ListboxState.InvisibleUnmounted })
+ assertListbox({ state: ListboxState.InvisibleUnmounted })
// Verify we got the change event
expect(handleChange).toHaveBeenCalledTimes(1)
@@ -1248,10 +1329,10 @@ describe('Keyboard interactions', () => {
// Open listbox
await press(Keys.Space)
- // Verify it is open
- assertListboxButton({ state: ListboxState.Open })
+ // Verify it is visible
+ assertListboxButton({ state: ListboxState.Visible })
assertListbox({
- state: ListboxState.Open,
+ state: ListboxState.Visible,
attributes: { id: 'headlessui-listbox-options-2' },
})
assertActiveElement(getListbox())
@@ -1261,8 +1342,8 @@ describe('Keyboard interactions', () => {
await press(Keys.Escape)
// Verify it is closed
- assertListboxButton({ state: ListboxState.Closed })
- assertListbox({ state: ListboxState.Closed })
+ assertListboxButton({ state: ListboxState.InvisibleUnmounted })
+ assertListbox({ state: ListboxState.InvisibleUnmounted })
// Verify the button is focused again
assertActiveElement(getListboxButton())
@@ -1286,10 +1367,10 @@ describe('Keyboard interactions', () => {
)
assertListboxButton({
- state: ListboxState.Closed,
+ state: ListboxState.InvisibleUnmounted,
attributes: { id: 'headlessui-listbox-button-1' },
})
- assertListbox({ state: ListboxState.Closed })
+ assertListbox({ state: ListboxState.InvisibleUnmounted })
// Focus the button
getListboxButton()?.focus()
@@ -1297,10 +1378,10 @@ describe('Keyboard interactions', () => {
// Open listbox
await press(Keys.Enter)
- // Verify it is open
- assertListboxButton({ state: ListboxState.Open })
+ // Verify it is visible
+ assertListboxButton({ state: ListboxState.Visible })
assertListbox({
- state: ListboxState.Open,
+ state: ListboxState.Visible,
attributes: { id: 'headlessui-listbox-options-2' },
})
assertActiveElement(getListbox())
@@ -1316,8 +1397,8 @@ describe('Keyboard interactions', () => {
await press(Keys.Tab)
// Verify it is still open
- assertListboxButton({ state: ListboxState.Open })
- assertListbox({ state: ListboxState.Open })
+ assertListboxButton({ state: ListboxState.Visible })
+ assertListbox({ state: ListboxState.Visible })
assertActiveElement(getListbox())
})
)
@@ -1337,10 +1418,10 @@ describe('Keyboard interactions', () => {
)
assertListboxButton({
- state: ListboxState.Closed,
+ state: ListboxState.InvisibleUnmounted,
attributes: { id: 'headlessui-listbox-button-1' },
})
- assertListbox({ state: ListboxState.Closed })
+ assertListbox({ state: ListboxState.InvisibleUnmounted })
// Focus the button
getListboxButton()?.focus()
@@ -1348,10 +1429,10 @@ describe('Keyboard interactions', () => {
// Open listbox
await press(Keys.Enter)
- // Verify it is open
- assertListboxButton({ state: ListboxState.Open })
+ // Verify it is visible
+ assertListboxButton({ state: ListboxState.Visible })
assertListbox({
- state: ListboxState.Open,
+ state: ListboxState.Visible,
attributes: { id: 'headlessui-listbox-options-2' },
})
assertActiveElement(getListbox())
@@ -1367,8 +1448,8 @@ describe('Keyboard interactions', () => {
await press(shift(Keys.Tab))
// Verify it is still open
- assertListboxButton({ state: ListboxState.Open })
- assertListbox({ state: ListboxState.Open })
+ assertListboxButton({ state: ListboxState.Visible })
+ assertListbox({ state: ListboxState.Visible })
assertActiveElement(getListbox())
})
)
@@ -1390,10 +1471,10 @@ describe('Keyboard interactions', () => {
)
assertListboxButton({
- state: ListboxState.Closed,
+ state: ListboxState.InvisibleUnmounted,
attributes: { id: 'headlessui-listbox-button-1' },
})
- assertListbox({ state: ListboxState.Closed })
+ assertListbox({ state: ListboxState.InvisibleUnmounted })
// Focus the button
getListboxButton()?.focus()
@@ -1401,10 +1482,10 @@ describe('Keyboard interactions', () => {
// Open listbox
await press(Keys.ArrowDown)
- // Verify it is open
- assertListboxButton({ state: ListboxState.Open })
+ // Verify it is visible
+ assertListboxButton({ state: ListboxState.Visible })
assertListbox({
- state: ListboxState.Open,
+ state: ListboxState.Visible,
attributes: { id: 'headlessui-listbox-options-2' },
})
assertActiveElement(getListbox())
@@ -1435,10 +1516,10 @@ describe('Keyboard interactions', () => {
)
assertListboxButton({
- state: ListboxState.Closed,
+ state: ListboxState.InvisibleUnmounted,
attributes: { id: 'headlessui-listbox-button-1' },
})
- assertListbox({ state: ListboxState.Closed })
+ assertListbox({ state: ListboxState.InvisibleUnmounted })
// Focus the button
getListboxButton()?.focus()
@@ -1448,10 +1529,10 @@ describe('Keyboard interactions', () => {
// Verify it is still closed
assertListboxButton({
- state: ListboxState.Closed,
+ state: ListboxState.InvisibleUnmounted,
attributes: { id: 'headlessui-listbox-button-1' },
})
- assertListbox({ state: ListboxState.Closed })
+ assertListbox({ state: ListboxState.InvisibleUnmounted })
})
)
@@ -1470,10 +1551,10 @@ describe('Keyboard interactions', () => {
)
assertListboxButton({
- state: ListboxState.Closed,
+ state: ListboxState.InvisibleUnmounted,
attributes: { id: 'headlessui-listbox-button-1' },
})
- assertListbox({ state: ListboxState.Closed })
+ assertListbox({ state: ListboxState.InvisibleUnmounted })
// Focus the button
getListboxButton()?.focus()
@@ -1481,10 +1562,10 @@ describe('Keyboard interactions', () => {
// Open listbox
await press(Keys.ArrowDown)
- // Verify it is open
- assertListboxButton({ state: ListboxState.Open })
+ // Verify it is visible
+ assertListboxButton({ state: ListboxState.Visible })
assertListbox({
- state: ListboxState.Open,
+ state: ListboxState.Visible,
attributes: { id: 'headlessui-listbox-options-2' },
})
assertActiveElement(getListbox())
@@ -1510,14 +1591,14 @@ describe('Keyboard interactions', () => {
)
- assertListbox({ state: ListboxState.Closed })
+ assertListbox({ state: ListboxState.InvisibleUnmounted })
// Focus the button
getListboxButton()?.focus()
// Open listbox
await press(Keys.ArrowDown)
- assertListbox({ state: ListboxState.Open })
+ assertListbox({ state: ListboxState.Visible })
assertActiveElement(getListbox())
assertNoActiveListboxOption()
@@ -1539,10 +1620,10 @@ describe('Keyboard interactions', () => {
)
assertListboxButton({
- state: ListboxState.Closed,
+ state: ListboxState.InvisibleUnmounted,
attributes: { id: 'headlessui-listbox-button-1' },
})
- assertListbox({ state: ListboxState.Closed })
+ assertListbox({ state: ListboxState.InvisibleUnmounted })
// Focus the button
getListboxButton()?.focus()
@@ -1587,10 +1668,10 @@ describe('Keyboard interactions', () => {
)
assertListboxButton({
- state: ListboxState.Closed,
+ state: ListboxState.InvisibleUnmounted,
attributes: { id: 'headlessui-listbox-button-1' },
})
- assertListbox({ state: ListboxState.Closed })
+ assertListbox({ state: ListboxState.InvisibleUnmounted })
// Focus the button
getListboxButton()?.focus()
@@ -1629,10 +1710,10 @@ describe('Keyboard interactions', () => {
)
assertListboxButton({
- state: ListboxState.Closed,
+ state: ListboxState.InvisibleUnmounted,
attributes: { id: 'headlessui-listbox-button-1' },
})
- assertListbox({ state: ListboxState.Closed })
+ assertListbox({ state: ListboxState.InvisibleUnmounted })
// Focus the button
getListboxButton()?.focus()
@@ -1665,10 +1746,10 @@ describe('Keyboard interactions', () => {
)
assertListboxButton({
- state: ListboxState.Closed,
+ state: ListboxState.InvisibleUnmounted,
attributes: { id: 'headlessui-listbox-button-1' },
})
- assertListbox({ state: ListboxState.Closed })
+ assertListbox({ state: ListboxState.InvisibleUnmounted })
// Focus the button
getListboxButton()?.focus()
@@ -1676,10 +1757,10 @@ describe('Keyboard interactions', () => {
// Open listbox
await press(Keys.ArrowUp)
- // Verify it is open
- assertListboxButton({ state: ListboxState.Open })
+ // Verify it is visible
+ assertListboxButton({ state: ListboxState.Visible })
assertListbox({
- state: ListboxState.Open,
+ state: ListboxState.Visible,
attributes: { id: 'headlessui-listbox-options-2' },
})
assertActiveElement(getListbox())
@@ -1710,10 +1791,10 @@ describe('Keyboard interactions', () => {
)
assertListboxButton({
- state: ListboxState.Closed,
+ state: ListboxState.InvisibleUnmounted,
attributes: { id: 'headlessui-listbox-button-1' },
})
- assertListbox({ state: ListboxState.Closed })
+ assertListbox({ state: ListboxState.InvisibleUnmounted })
// Focus the button
getListboxButton()?.focus()
@@ -1723,10 +1804,10 @@ describe('Keyboard interactions', () => {
// Verify it is still closed
assertListboxButton({
- state: ListboxState.Closed,
+ state: ListboxState.InvisibleUnmounted,
attributes: { id: 'headlessui-listbox-button-1' },
})
- assertListbox({ state: ListboxState.Closed })
+ assertListbox({ state: ListboxState.InvisibleUnmounted })
})
)
@@ -1745,10 +1826,10 @@ describe('Keyboard interactions', () => {
)
assertListboxButton({
- state: ListboxState.Closed,
+ state: ListboxState.InvisibleUnmounted,
attributes: { id: 'headlessui-listbox-button-1' },
})
- assertListbox({ state: ListboxState.Closed })
+ assertListbox({ state: ListboxState.InvisibleUnmounted })
// Focus the button
getListboxButton()?.focus()
@@ -1756,10 +1837,10 @@ describe('Keyboard interactions', () => {
// Open listbox
await press(Keys.ArrowUp)
- // Verify it is open
- assertListboxButton({ state: ListboxState.Open })
+ // Verify it is visible
+ assertListboxButton({ state: ListboxState.Visible })
assertListbox({
- state: ListboxState.Open,
+ state: ListboxState.Visible,
attributes: { id: 'headlessui-listbox-options-2' },
})
assertActiveElement(getListbox())
@@ -1785,14 +1866,14 @@ describe('Keyboard interactions', () => {
)
- assertListbox({ state: ListboxState.Closed })
+ assertListbox({ state: ListboxState.InvisibleUnmounted })
// Focus the button
getListboxButton()?.focus()
// Open listbox
await press(Keys.ArrowUp)
- assertListbox({ state: ListboxState.Open })
+ assertListbox({ state: ListboxState.Visible })
assertActiveElement(getListbox())
assertNoActiveListboxOption()
@@ -1818,10 +1899,10 @@ describe('Keyboard interactions', () => {
)
assertListboxButton({
- state: ListboxState.Closed,
+ state: ListboxState.InvisibleUnmounted,
attributes: { id: 'headlessui-listbox-button-1' },
})
- assertListbox({ state: ListboxState.Closed })
+ assertListbox({ state: ListboxState.InvisibleUnmounted })
// Focus the button
getListboxButton()?.focus()
@@ -1856,10 +1937,10 @@ describe('Keyboard interactions', () => {
)
assertListboxButton({
- state: ListboxState.Closed,
+ state: ListboxState.InvisibleUnmounted,
attributes: { id: 'headlessui-listbox-button-1' },
})
- assertListbox({ state: ListboxState.Closed })
+ assertListbox({ state: ListboxState.InvisibleUnmounted })
// Focus the button
getListboxButton()?.focus()
@@ -1898,10 +1979,10 @@ describe('Keyboard interactions', () => {
)
assertListboxButton({
- state: ListboxState.Closed,
+ state: ListboxState.InvisibleUnmounted,
attributes: { id: 'headlessui-listbox-button-1' },
})
- assertListbox({ state: ListboxState.Closed })
+ assertListbox({ state: ListboxState.InvisibleUnmounted })
// Focus the button
getListboxButton()?.focus()
@@ -1909,10 +1990,10 @@ describe('Keyboard interactions', () => {
// Open listbox
await press(Keys.ArrowUp)
- // Verify it is open
- assertListboxButton({ state: ListboxState.Open })
+ // Verify it is visible
+ assertListboxButton({ state: ListboxState.Visible })
assertListbox({
- state: ListboxState.Open,
+ state: ListboxState.Visible,
attributes: { id: 'headlessui-listbox-options-2' },
})
assertActiveElement(getListbox())
@@ -2688,18 +2769,18 @@ describe('Mouse interactions', () => {
)
assertListboxButton({
- state: ListboxState.Closed,
+ state: ListboxState.InvisibleUnmounted,
attributes: { id: 'headlessui-listbox-button-1' },
})
- assertListbox({ state: ListboxState.Closed })
+ assertListbox({ state: ListboxState.InvisibleUnmounted })
// Open listbox
await click(getListboxButton())
- // Verify it is open
- assertListboxButton({ state: ListboxState.Open })
+ // Verify it is visible
+ assertListboxButton({ state: ListboxState.Visible })
assertListbox({
- state: ListboxState.Open,
+ state: ListboxState.Visible,
attributes: { id: 'headlessui-listbox-options-2' },
})
assertActiveElement(getListbox())
@@ -2727,20 +2808,20 @@ describe('Mouse interactions', () => {
)
assertListboxButton({
- state: ListboxState.Closed,
+ state: ListboxState.InvisibleUnmounted,
attributes: { id: 'headlessui-listbox-button-1' },
})
- assertListbox({ state: ListboxState.Closed })
+ assertListbox({ state: ListboxState.InvisibleUnmounted })
// Try to open the listbox
await click(getListboxButton())
// Verify it is still closed
assertListboxButton({
- state: ListboxState.Closed,
+ state: ListboxState.InvisibleUnmounted,
attributes: { id: 'headlessui-listbox-button-1' },
})
- assertListbox({ state: ListboxState.Closed })
+ assertListbox({ state: ListboxState.InvisibleUnmounted })
})
)
@@ -2759,18 +2840,18 @@ describe('Mouse interactions', () => {
)
assertListboxButton({
- state: ListboxState.Closed,
+ state: ListboxState.InvisibleUnmounted,
attributes: { id: 'headlessui-listbox-button-1' },
})
- assertListbox({ state: ListboxState.Closed })
+ assertListbox({ state: ListboxState.InvisibleUnmounted })
// Open listbox
await click(getListboxButton())
- // Verify it is open
- assertListboxButton({ state: ListboxState.Open })
+ // Verify it is visible
+ assertListboxButton({ state: ListboxState.Visible })
assertListbox({
- state: ListboxState.Open,
+ state: ListboxState.Visible,
attributes: { id: 'headlessui-listbox-options-2' },
})
assertActiveElement(getListbox())
@@ -2803,15 +2884,15 @@ describe('Mouse interactions', () => {
// Open listbox
await click(getListboxButton())
- // Verify it is open
- assertListboxButton({ state: ListboxState.Open })
+ // Verify it is visible
+ assertListboxButton({ state: ListboxState.Visible })
// Click to close
await click(getListboxButton())
// Verify it is closed
- assertListboxButton({ state: ListboxState.Closed })
- assertListbox({ state: ListboxState.Closed })
+ assertListboxButton({ state: ListboxState.InvisibleUnmounted })
+ assertListbox({ state: ListboxState.InvisibleUnmounted })
})
)
@@ -2855,13 +2936,13 @@ describe('Mouse interactions', () => {
)
// Verify that the window is closed
- assertListbox({ state: ListboxState.Closed })
+ assertListbox({ state: ListboxState.InvisibleUnmounted })
// Click something that is not related to the listbox
await click(document.body)
// Should still be closed
- assertListbox({ state: ListboxState.Closed })
+ assertListbox({ state: ListboxState.InvisibleUnmounted })
})
)
@@ -2881,14 +2962,14 @@ describe('Mouse interactions', () => {
// Open listbox
await click(getListboxButton())
- assertListbox({ state: ListboxState.Open })
+ assertListbox({ state: ListboxState.Visible })
assertActiveElement(getListbox())
// Click something that is not related to the listbox
await click(document.body)
// Should be closed now
- assertListbox({ state: ListboxState.Closed })
+ assertListbox({ state: ListboxState.InvisibleUnmounted })
// Verify the button is focused again
assertActiveElement(getListboxButton())
@@ -2955,14 +3036,14 @@ describe('Mouse interactions', () => {
// Open listbox
await click(getListboxButton())
- assertListbox({ state: ListboxState.Open })
+ assertListbox({ state: ListboxState.Visible })
assertActiveElement(getListbox())
// Click the listbox button again
await click(getListboxButton())
// Should be closed now
- assertListbox({ state: ListboxState.Closed })
+ assertListbox({ state: ListboxState.InvisibleUnmounted })
// Verify the button is focused again
assertActiveElement(getListboxButton())
@@ -3211,14 +3292,14 @@ describe('Mouse interactions', () => {
// Open listbox
await click(getListboxButton())
- assertListbox({ state: ListboxState.Open })
+ assertListbox({ state: ListboxState.Visible })
assertActiveElement(getListbox())
const options = getListboxOptions()
// We should be able to click the first option
await click(options[1])
- assertListbox({ state: ListboxState.Closed })
+ assertListbox({ state: ListboxState.InvisibleUnmounted })
expect(handleChange).toHaveBeenCalledTimes(1)
expect(handleChange).toHaveBeenCalledWith('bob')
@@ -3264,14 +3345,14 @@ describe('Mouse interactions', () => {
// Open listbox
await click(getListboxButton())
- assertListbox({ state: ListboxState.Open })
+ assertListbox({ state: ListboxState.Visible })
assertActiveElement(getListbox())
const options = getListboxOptions()
// We should be able to click the first option
await click(options[1])
- assertListbox({ state: ListboxState.Open })
+ assertListbox({ state: ListboxState.Visible })
assertActiveElement(getListbox())
expect(handleChange).toHaveBeenCalledTimes(0)
@@ -3302,7 +3383,7 @@ describe('Mouse interactions', () => {
// Open listbox
await click(getListboxButton())
- assertListbox({ state: ListboxState.Open })
+ assertListbox({ state: ListboxState.Visible })
assertActiveElement(getListbox())
const options = getListboxOptions()
@@ -3334,7 +3415,7 @@ describe('Mouse interactions', () => {
// Open listbox
await click(getListboxButton())
- assertListbox({ state: ListboxState.Open })
+ assertListbox({ state: ListboxState.Visible })
assertActiveElement(getListbox())
const options = getListboxOptions()
diff --git a/packages/@headlessui-react/src/components/listbox/listbox.tsx b/packages/@headlessui-react/src/components/listbox/listbox.tsx
index 031239d..dc76c23 100644
--- a/packages/@headlessui-react/src/components/listbox/listbox.tsx
+++ b/packages/@headlessui-react/src/components/listbox/listbox.tsx
@@ -6,7 +6,7 @@ import { useIsoMorphicEffect } from '../../hooks/use-iso-morphic-effect'
import { useComputed } from '../../hooks/use-computed'
import { useSyncRefs } from '../../hooks/use-sync-refs'
import { Props } from '../../types'
-import { forwardRefWithAs, render } from '../../utils/render'
+import { Features, forwardRefWithAs, PropsForFeatures, render } from '../../utils/render'
import { match } from '../../utils/match'
import { disposables } from '../../utils/disposables'
import { Keys } from '../keyboard'
@@ -114,7 +114,11 @@ const reducers: {
action: Extract
) => StateDefinition
} = {
- [ActionTypes.CloseListbox]: state => ({ ...state, listboxState: ListboxStates.Closed }),
+ [ActionTypes.CloseListbox]: state => ({
+ ...state,
+ activeOptionIndex: null,
+ listboxState: ListboxStates.Closed,
+ }),
[ActionTypes.OpenListbox]: state => ({ ...state, listboxState: ListboxStates.Open }),
[ActionTypes.GoToOption]: (state, action) => {
const activeOptionIndex = calculateActiveOptionIndex(state, action.focus, action.id)
@@ -375,11 +379,7 @@ function Label(
() => ({ open: state.listboxState === ListboxStates.Open }),
[state]
)
- const propsWeControl = {
- ref: state.labelRef,
- id,
- onPointerUp: handlePointerUp,
- }
+ const propsWeControl = { ref: state.labelRef, id, onPointerUp: handlePointerUp }
return render({ ...props, ...propsWeControl }, propsBag, DEFAULT_LABEL_TAG)
}
@@ -397,24 +397,15 @@ type OptionsPropsWeControl =
const DEFAULT_OPTIONS_TAG = 'ul'
type OptionsRenderPropArg = { open: boolean }
-
-type ListboxOptionsProp = Props & {
- static?: boolean
-}
+const OptionsRenderFeatures = Features.RenderStrategy | Features.Static
const Options = forwardRefWithAs(function Options<
TTag extends React.ElementType = typeof DEFAULT_OPTIONS_TAG
->(props: ListboxOptionsProp, ref: React.Ref) {
- const {
- enter,
- enterFrom,
- enterTo,
- leave,
- leaveFrom,
- leaveTo,
- static: isStatic = false,
- ...passthroughProps
- } = props
+>(
+ props: Props &
+ PropsForFeatures,
+ ref: React.Ref
+) {
const [state, dispatch] = useListboxContext([Listbox.name, Options.name].join('.'))
const optionsRef = useSyncRefs(state.optionsRef, ref)
@@ -500,14 +491,16 @@ const Options = forwardRefWithAs(function Options<
onKeyDown: handleKeyDown,
role: 'listbox',
tabIndex: 0,
+ ref: optionsRef,
}
-
- if (!isStatic && state.listboxState === ListboxStates.Closed) return null
+ const passthroughProps = props
return render(
- { ...passthroughProps, ...propsWeControl, ...{ ref: optionsRef } },
+ { ...passthroughProps, ...propsWeControl },
propsBag,
- DEFAULT_OPTIONS_TAG
+ DEFAULT_OPTIONS_TAG,
+ OptionsRenderFeatures,
+ state.listboxState === ListboxStates.Open
)
})
@@ -570,17 +563,19 @@ function Option<
}, [bag, id])
useIsoMorphicEffect(() => {
+ if (state.listboxState !== ListboxStates.Open) return
if (!selected) return
dispatch({ type: ActionTypes.GoToOption, focus: Focus.Specific, id })
document.getElementById(id)?.focus?.()
- }, [])
+ }, [state.listboxState])
useIsoMorphicEffect(() => {
+ if (state.listboxState !== ListboxStates.Open) return
if (!active) return
const d = disposables()
d.nextFrame(() => document.getElementById(id)?.scrollIntoView?.({ block: 'nearest' }))
return d.dispose
- }, [active])
+ }, [active, state.listboxState])
const handleClick = React.useCallback(
(event: { preventDefault: Function }) => {
@@ -627,11 +622,7 @@ function Option<
onPointerLeave: handlePointerLeave,
}
- return render(
- { ...passthroughProps, ...propsWeControl },
- propsBag,
- DEFAULT_OPTION_TAG
- )
+ return render({ ...passthroughProps, ...propsWeControl }, propsBag, DEFAULT_OPTION_TAG)
}
function resolvePropValue(property: TProperty, bag: TBag) {
diff --git a/packages/@headlessui-react/src/components/menu/menu.test.tsx b/packages/@headlessui-react/src/components/menu/menu.test.tsx
index 055bc6d..d0fb46f 100644
--- a/packages/@headlessui-react/src/components/menu/menu.test.tsx
+++ b/packages/@headlessui-react/src/components/menu/menu.test.tsx
@@ -68,10 +68,10 @@ describe('Safe guards', () => {
)
assertMenuButton({
- state: MenuState.Closed,
+ state: MenuState.InvisibleUnmounted,
attributes: { id: 'headlessui-menu-button-1' },
})
- assertMenu({ state: MenuState.Closed })
+ assertMenu({ state: MenuState.InvisibleUnmounted })
})
)
})
@@ -99,18 +99,18 @@ describe('Rendering', () => {
)
assertMenuButton({
- state: MenuState.Closed,
+ state: MenuState.InvisibleUnmounted,
attributes: { id: 'headlessui-menu-button-1' },
})
- assertMenu({ state: MenuState.Closed })
+ assertMenu({ state: MenuState.InvisibleUnmounted })
await click(getMenuButton())
assertMenuButton({
- state: MenuState.Open,
+ state: MenuState.Visible,
attributes: { id: 'headlessui-menu-button-1' },
})
- assertMenu({ state: MenuState.Open })
+ assertMenu({ state: MenuState.Visible })
})
)
})
@@ -131,20 +131,20 @@ describe('Rendering', () => {
)
assertMenuButton({
- state: MenuState.Closed,
+ state: MenuState.InvisibleUnmounted,
attributes: { id: 'headlessui-menu-button-1' },
textContent: JSON.stringify({ open: false, focused: false }),
})
- assertMenu({ state: MenuState.Closed })
+ assertMenu({ state: MenuState.InvisibleUnmounted })
await click(getMenuButton())
assertMenuButton({
- state: MenuState.Open,
+ state: MenuState.Visible,
attributes: { id: 'headlessui-menu-button-1' },
textContent: JSON.stringify({ open: true, focused: false }),
})
- assertMenu({ state: MenuState.Open })
+ assertMenu({ state: MenuState.Visible })
})
)
@@ -165,20 +165,20 @@ describe('Rendering', () => {
)
assertMenuButton({
- state: MenuState.Closed,
+ state: MenuState.InvisibleUnmounted,
attributes: { id: 'headlessui-menu-button-1' },
textContent: JSON.stringify({ open: false, focused: false }),
})
- assertMenu({ state: MenuState.Closed })
+ assertMenu({ state: MenuState.InvisibleUnmounted })
await click(getMenuButton())
assertMenuButton({
- state: MenuState.Open,
+ state: MenuState.Visible,
attributes: { id: 'headlessui-menu-button-1' },
textContent: JSON.stringify({ open: true, focused: false }),
})
- assertMenu({ state: MenuState.Open })
+ assertMenu({ state: MenuState.Visible })
})
)
})
@@ -201,19 +201,19 @@ describe('Rendering', () => {
)
assertMenuButton({
- state: MenuState.Closed,
+ state: MenuState.InvisibleUnmounted,
attributes: { id: 'headlessui-menu-button-1' },
})
- assertMenu({ state: MenuState.Closed })
+ assertMenu({ state: MenuState.InvisibleUnmounted })
await click(getMenuButton())
assertMenuButton({
- state: MenuState.Open,
+ state: MenuState.Visible,
attributes: { id: 'headlessui-menu-button-1' },
})
assertMenu({
- state: MenuState.Open,
+ state: MenuState.Visible,
textContent: JSON.stringify({ open: true }),
})
})
@@ -234,6 +234,26 @@ describe('Rendering', () => {
// Let's verify that the Menu is already there
expect(getMenu()).not.toBe(null)
})
+
+ it('should be possible to use a different render strategy for the Menu.Items', async () => {
+ render(
+
+ Trigger
+
+ Item A
+ Item B
+ Item C
+
+
+ )
+
+ assertMenu({ state: MenuState.InvisibleHidden })
+
+ // Let's open the Menu, to see if it is not hidden anymore
+ await click(getMenuButton())
+
+ assertMenu({ state: MenuState.Visible })
+ })
})
describe('Menu.Item', () => {
@@ -250,19 +270,19 @@ describe('Rendering', () => {
)
assertMenuButton({
- state: MenuState.Closed,
+ state: MenuState.InvisibleUnmounted,
attributes: { id: 'headlessui-menu-button-1' },
})
- assertMenu({ state: MenuState.Closed })
+ assertMenu({ state: MenuState.InvisibleUnmounted })
await click(getMenuButton())
assertMenuButton({
- state: MenuState.Open,
+ state: MenuState.Visible,
attributes: { id: 'headlessui-menu-button-1' },
})
assertMenu({
- state: MenuState.Open,
+ state: MenuState.Visible,
textContent: JSON.stringify({ active: false, disabled: false }),
})
})
@@ -292,10 +312,10 @@ describe('Rendering composition', () => {
)
assertMenuButton({
- state: MenuState.Closed,
+ state: MenuState.InvisibleUnmounted,
attributes: { id: 'headlessui-menu-button-1' },
})
- assertMenu({ state: MenuState.Closed })
+ assertMenu({ state: MenuState.InvisibleUnmounted })
// Open menu
await click(getMenuButton())
@@ -349,10 +369,10 @@ describe('Rendering composition', () => {
)
assertMenuButton({
- state: MenuState.Closed,
+ state: MenuState.InvisibleUnmounted,
attributes: { id: 'headlessui-menu-button-1' },
})
- assertMenu({ state: MenuState.Closed })
+ assertMenu({ state: MenuState.InvisibleUnmounted })
// Open menu
await click(getMenuButton())
@@ -381,10 +401,10 @@ describe('Keyboard interactions', () => {
)
assertMenuButton({
- state: MenuState.Closed,
+ state: MenuState.InvisibleUnmounted,
attributes: { id: 'headlessui-menu-button-1' },
})
- assertMenu({ state: MenuState.Closed })
+ assertMenu({ state: MenuState.InvisibleUnmounted })
// Focus the button
getMenuButton()?.focus()
@@ -393,9 +413,9 @@ describe('Keyboard interactions', () => {
await press(Keys.Enter)
// Verify it is open
- assertMenuButton({ state: MenuState.Open })
+ assertMenuButton({ state: MenuState.Visible })
assertMenu({
- state: MenuState.Open,
+ state: MenuState.Visible,
attributes: { id: 'headlessui-menu-items-2' },
})
assertMenuButtonLinkedWithMenu()
@@ -425,10 +445,10 @@ describe('Keyboard interactions', () => {
)
assertMenuButton({
- state: MenuState.Closed,
+ state: MenuState.InvisibleUnmounted,
attributes: { id: 'headlessui-menu-button-1' },
})
- assertMenu({ state: MenuState.Closed })
+ assertMenu({ state: MenuState.InvisibleUnmounted })
// Focus the button
getMenuButton()?.focus()
@@ -438,10 +458,10 @@ describe('Keyboard interactions', () => {
// Verify it is still closed
assertMenuButton({
- state: MenuState.Closed,
+ state: MenuState.InvisibleUnmounted,
attributes: { id: 'headlessui-menu-button-1' },
})
- assertMenu({ state: MenuState.Closed })
+ assertMenu({ state: MenuState.InvisibleUnmounted })
})
)
@@ -455,14 +475,14 @@ describe('Keyboard interactions', () => {
)
- assertMenu({ state: MenuState.Closed })
+ assertMenu({ state: MenuState.InvisibleUnmounted })
// Focus the button
getMenuButton()?.focus()
// Open menu
await press(Keys.Enter)
- assertMenu({ state: MenuState.Open })
+ assertMenu({ state: MenuState.Visible })
assertNoActiveMenuItem()
})
@@ -485,10 +505,10 @@ describe('Keyboard interactions', () => {
)
assertMenuButton({
- state: MenuState.Closed,
+ state: MenuState.InvisibleUnmounted,
attributes: { id: 'headlessui-menu-button-1' },
})
- assertMenu({ state: MenuState.Closed })
+ assertMenu({ state: MenuState.InvisibleUnmounted })
// Focus the button
getMenuButton()?.focus()
@@ -522,10 +542,10 @@ describe('Keyboard interactions', () => {
)
assertMenuButton({
- state: MenuState.Closed,
+ state: MenuState.InvisibleUnmounted,
attributes: { id: 'headlessui-menu-button-1' },
})
- assertMenu({ state: MenuState.Closed })
+ assertMenu({ state: MenuState.InvisibleUnmounted })
// Focus the button
getMenuButton()?.focus()
@@ -561,10 +581,10 @@ describe('Keyboard interactions', () => {
)
assertMenuButton({
- state: MenuState.Closed,
+ state: MenuState.InvisibleUnmounted,
attributes: { id: 'headlessui-menu-button-1' },
})
- assertMenu({ state: MenuState.Closed })
+ assertMenu({ state: MenuState.InvisibleUnmounted })
// Focus the button
getMenuButton()?.focus()
@@ -591,23 +611,23 @@ describe('Keyboard interactions', () => {
)
assertMenuButton({
- state: MenuState.Closed,
+ state: MenuState.InvisibleUnmounted,
attributes: { id: 'headlessui-menu-button-1' },
})
- assertMenu({ state: MenuState.Closed })
+ assertMenu({ state: MenuState.InvisibleUnmounted })
// Open menu
await click(getMenuButton())
// Verify it is open
- assertMenuButton({ state: MenuState.Open })
+ assertMenuButton({ state: MenuState.Visible })
// Close menu
await press(Keys.Enter)
// Verify it is closed
- assertMenuButton({ state: MenuState.Closed })
- assertMenu({ state: MenuState.Closed })
+ assertMenuButton({ state: MenuState.InvisibleUnmounted })
+ assertMenu({ state: MenuState.InvisibleUnmounted })
// Verify the button is focused again
assertActiveElement(getMenuButton())
@@ -632,16 +652,16 @@ describe('Keyboard interactions', () => {
)
assertMenuButton({
- state: MenuState.Closed,
+ state: MenuState.InvisibleUnmounted,
attributes: { id: 'headlessui-menu-button-1' },
})
- assertMenu({ state: MenuState.Closed })
+ assertMenu({ state: MenuState.InvisibleUnmounted })
// Open menu
await click(getMenuButton())
// Verify it is open
- assertMenuButton({ state: MenuState.Open })
+ assertMenuButton({ state: MenuState.Visible })
// Activate the first menu item
const items = getMenuItems()
@@ -651,8 +671,8 @@ describe('Keyboard interactions', () => {
await press(Keys.Enter)
// Verify it is closed
- assertMenuButton({ state: MenuState.Closed })
- assertMenu({ state: MenuState.Closed })
+ assertMenuButton({ state: MenuState.InvisibleUnmounted })
+ assertMenu({ state: MenuState.InvisibleUnmounted })
// Verify the button is focused again
assertActiveElement(getMenuButton())
@@ -684,16 +704,16 @@ describe('Keyboard interactions', () => {
)
assertMenuButton({
- state: MenuState.Closed,
+ state: MenuState.InvisibleUnmounted,
attributes: { id: 'headlessui-menu-button-1' },
})
- assertMenu({ state: MenuState.Closed })
+ assertMenu({ state: MenuState.InvisibleUnmounted })
// Open menu
await click(getMenuButton())
// Verify it is open
- assertMenuButton({ state: MenuState.Open })
+ assertMenuButton({ state: MenuState.Visible })
// Activate the second menu item
const items = getMenuItems()
@@ -703,8 +723,8 @@ describe('Keyboard interactions', () => {
await press(Keys.Enter)
// Verify it is closed
- assertMenuButton({ state: MenuState.Closed })
- assertMenu({ state: MenuState.Closed })
+ assertMenuButton({ state: MenuState.InvisibleUnmounted })
+ assertMenu({ state: MenuState.InvisibleUnmounted })
// Verify the button got "clicked"
expect(clickHandler).toHaveBeenCalledTimes(1)
@@ -745,10 +765,10 @@ describe('Keyboard interactions', () => {
)
assertMenuButton({
- state: MenuState.Closed,
+ state: MenuState.InvisibleUnmounted,
attributes: { id: 'headlessui-menu-button-1' },
})
- assertMenu({ state: MenuState.Closed })
+ assertMenu({ state: MenuState.InvisibleUnmounted })
// Focus the button
getMenuButton()?.focus()
@@ -757,9 +777,9 @@ describe('Keyboard interactions', () => {
await press(Keys.Space)
// Verify it is open
- assertMenuButton({ state: MenuState.Open })
+ assertMenuButton({ state: MenuState.Visible })
assertMenu({
- state: MenuState.Open,
+ state: MenuState.Visible,
attributes: { id: 'headlessui-menu-items-2' },
})
assertMenuButtonLinkedWithMenu()
@@ -787,10 +807,10 @@ describe('Keyboard interactions', () => {
)
assertMenuButton({
- state: MenuState.Closed,
+ state: MenuState.InvisibleUnmounted,
attributes: { id: 'headlessui-menu-button-1' },
})
- assertMenu({ state: MenuState.Closed })
+ assertMenu({ state: MenuState.InvisibleUnmounted })
// Focus the button
getMenuButton()?.focus()
@@ -800,10 +820,10 @@ describe('Keyboard interactions', () => {
// Verify it is still closed
assertMenuButton({
- state: MenuState.Closed,
+ state: MenuState.InvisibleUnmounted,
attributes: { id: 'headlessui-menu-button-1' },
})
- assertMenu({ state: MenuState.Closed })
+ assertMenu({ state: MenuState.InvisibleUnmounted })
})
)
@@ -817,14 +837,14 @@ describe('Keyboard interactions', () => {
)
- assertMenu({ state: MenuState.Closed })
+ assertMenu({ state: MenuState.InvisibleUnmounted })
// Focus the button
getMenuButton()?.focus()
// Open menu
await press(Keys.Space)
- assertMenu({ state: MenuState.Open })
+ assertMenu({ state: MenuState.Visible })
assertNoActiveMenuItem()
})
@@ -847,10 +867,10 @@ describe('Keyboard interactions', () => {
)
assertMenuButton({
- state: MenuState.Closed,
+ state: MenuState.InvisibleUnmounted,
attributes: { id: 'headlessui-menu-button-1' },
})
- assertMenu({ state: MenuState.Closed })
+ assertMenu({ state: MenuState.InvisibleUnmounted })
// Focus the button
getMenuButton()?.focus()
@@ -884,10 +904,10 @@ describe('Keyboard interactions', () => {
)
assertMenuButton({
- state: MenuState.Closed,
+ state: MenuState.InvisibleUnmounted,
attributes: { id: 'headlessui-menu-button-1' },
})
- assertMenu({ state: MenuState.Closed })
+ assertMenu({ state: MenuState.InvisibleUnmounted })
// Focus the button
getMenuButton()?.focus()
@@ -923,10 +943,10 @@ describe('Keyboard interactions', () => {
)
assertMenuButton({
- state: MenuState.Closed,
+ state: MenuState.InvisibleUnmounted,
attributes: { id: 'headlessui-menu-button-1' },
})
- assertMenu({ state: MenuState.Closed })
+ assertMenu({ state: MenuState.InvisibleUnmounted })
// Focus the button
getMenuButton()?.focus()
@@ -953,23 +973,23 @@ describe('Keyboard interactions', () => {
)
assertMenuButton({
- state: MenuState.Closed,
+ state: MenuState.InvisibleUnmounted,
attributes: { id: 'headlessui-menu-button-1' },
})
- assertMenu({ state: MenuState.Closed })
+ assertMenu({ state: MenuState.InvisibleUnmounted })
// Open menu
await click(getMenuButton())
// Verify it is open
- assertMenuButton({ state: MenuState.Open })
+ assertMenuButton({ state: MenuState.Visible })
// Close menu
await press(Keys.Space)
// Verify it is closed
- assertMenuButton({ state: MenuState.Closed })
- assertMenu({ state: MenuState.Closed })
+ assertMenuButton({ state: MenuState.InvisibleUnmounted })
+ assertMenu({ state: MenuState.InvisibleUnmounted })
// Verify the button is focused again
assertActiveElement(getMenuButton())
@@ -994,16 +1014,16 @@ describe('Keyboard interactions', () => {
)
assertMenuButton({
- state: MenuState.Closed,
+ state: MenuState.InvisibleUnmounted,
attributes: { id: 'headlessui-menu-button-1' },
})
- assertMenu({ state: MenuState.Closed })
+ assertMenu({ state: MenuState.InvisibleUnmounted })
// Open menu
await click(getMenuButton())
// Verify it is open
- assertMenuButton({ state: MenuState.Open })
+ assertMenuButton({ state: MenuState.Visible })
// Activate the first menu item
const items = getMenuItems()
@@ -1013,8 +1033,8 @@ describe('Keyboard interactions', () => {
await press(Keys.Space)
// Verify it is closed
- assertMenuButton({ state: MenuState.Closed })
- assertMenu({ state: MenuState.Closed })
+ assertMenuButton({ state: MenuState.InvisibleUnmounted })
+ assertMenu({ state: MenuState.InvisibleUnmounted })
// Verify the "click" went through on the `a` tag
expect(clickHandler).toHaveBeenCalled()
@@ -1047,9 +1067,9 @@ describe('Keyboard interactions', () => {
await press(Keys.Space)
// Verify it is open
- assertMenuButton({ state: MenuState.Open })
+ assertMenuButton({ state: MenuState.Visible })
assertMenu({
- state: MenuState.Open,
+ state: MenuState.Visible,
attributes: { id: 'headlessui-menu-items-2' },
})
assertMenuButtonLinkedWithMenu()
@@ -1058,8 +1078,8 @@ describe('Keyboard interactions', () => {
await press(Keys.Escape)
// Verify it is closed
- assertMenuButton({ state: MenuState.Closed })
- assertMenu({ state: MenuState.Closed })
+ assertMenuButton({ state: MenuState.InvisibleUnmounted })
+ assertMenu({ state: MenuState.InvisibleUnmounted })
// Verify the button is focused again
assertActiveElement(getMenuButton())
@@ -1083,10 +1103,10 @@ describe('Keyboard interactions', () => {
)
assertMenuButton({
- state: MenuState.Closed,
+ state: MenuState.InvisibleUnmounted,
attributes: { id: 'headlessui-menu-button-1' },
})
- assertMenu({ state: MenuState.Closed })
+ assertMenu({ state: MenuState.InvisibleUnmounted })
// Focus the button
getMenuButton()?.focus()
@@ -1095,9 +1115,9 @@ describe('Keyboard interactions', () => {
await press(Keys.Enter)
// Verify it is open
- assertMenuButton({ state: MenuState.Open })
+ assertMenuButton({ state: MenuState.Visible })
assertMenu({
- state: MenuState.Open,
+ state: MenuState.Visible,
attributes: { id: 'headlessui-menu-items-2' },
})
assertMenuButtonLinkedWithMenu()
@@ -1112,8 +1132,8 @@ describe('Keyboard interactions', () => {
await press(Keys.Tab)
// Verify it is still open
- assertMenuButton({ state: MenuState.Open })
- assertMenu({ state: MenuState.Open })
+ assertMenuButton({ state: MenuState.Visible })
+ assertMenu({ state: MenuState.Visible })
})
)
@@ -1132,10 +1152,10 @@ describe('Keyboard interactions', () => {
)
assertMenuButton({
- state: MenuState.Closed,
+ state: MenuState.InvisibleUnmounted,
attributes: { id: 'headlessui-menu-button-1' },
})
- assertMenu({ state: MenuState.Closed })
+ assertMenu({ state: MenuState.InvisibleUnmounted })
// Focus the button
getMenuButton()?.focus()
@@ -1144,9 +1164,9 @@ describe('Keyboard interactions', () => {
await press(Keys.Enter)
// Verify it is open
- assertMenuButton({ state: MenuState.Open })
+ assertMenuButton({ state: MenuState.Visible })
assertMenu({
- state: MenuState.Open,
+ state: MenuState.Visible,
attributes: { id: 'headlessui-menu-items-2' },
})
assertMenuButtonLinkedWithMenu()
@@ -1161,8 +1181,8 @@ describe('Keyboard interactions', () => {
await press(shift(Keys.Tab))
// Verify it is still open
- assertMenuButton({ state: MenuState.Open })
- assertMenu({ state: MenuState.Open })
+ assertMenuButton({ state: MenuState.Visible })
+ assertMenu({ state: MenuState.Visible })
})
)
})
@@ -1183,10 +1203,10 @@ describe('Keyboard interactions', () => {
)
assertMenuButton({
- state: MenuState.Closed,
+ state: MenuState.InvisibleUnmounted,
attributes: { id: 'headlessui-menu-button-1' },
})
- assertMenu({ state: MenuState.Closed })
+ assertMenu({ state: MenuState.InvisibleUnmounted })
// Focus the button
getMenuButton()?.focus()
@@ -1195,9 +1215,9 @@ describe('Keyboard interactions', () => {
await press(Keys.ArrowDown)
// Verify it is open
- assertMenuButton({ state: MenuState.Open })
+ assertMenuButton({ state: MenuState.Visible })
assertMenu({
- state: MenuState.Open,
+ state: MenuState.Visible,
attributes: { id: 'headlessui-menu-items-2' },
})
assertMenuButtonLinkedWithMenu()
@@ -1227,10 +1247,10 @@ describe('Keyboard interactions', () => {
)
assertMenuButton({
- state: MenuState.Closed,
+ state: MenuState.InvisibleUnmounted,
attributes: { id: 'headlessui-menu-button-1' },
})
- assertMenu({ state: MenuState.Closed })
+ assertMenu({ state: MenuState.InvisibleUnmounted })
// Focus the button
getMenuButton()?.focus()
@@ -1240,10 +1260,10 @@ describe('Keyboard interactions', () => {
// Verify it is still closed
assertMenuButton({
- state: MenuState.Closed,
+ state: MenuState.InvisibleUnmounted,
attributes: { id: 'headlessui-menu-button-1' },
})
- assertMenu({ state: MenuState.Closed })
+ assertMenu({ state: MenuState.InvisibleUnmounted })
})
)
@@ -1257,14 +1277,14 @@ describe('Keyboard interactions', () => {
)
- assertMenu({ state: MenuState.Closed })
+ assertMenu({ state: MenuState.InvisibleUnmounted })
// Focus the button
getMenuButton()?.focus()
// Open menu
await press(Keys.ArrowDown)
- assertMenu({ state: MenuState.Open })
+ assertMenu({ state: MenuState.Visible })
assertNoActiveMenuItem()
})
@@ -1285,10 +1305,10 @@ describe('Keyboard interactions', () => {
)
assertMenuButton({
- state: MenuState.Closed,
+ state: MenuState.InvisibleUnmounted,
attributes: { id: 'headlessui-menu-button-1' },
})
- assertMenu({ state: MenuState.Closed })
+ assertMenu({ state: MenuState.InvisibleUnmounted })
// Focus the button
getMenuButton()?.focus()
@@ -1333,10 +1353,10 @@ describe('Keyboard interactions', () => {
)
assertMenuButton({
- state: MenuState.Closed,
+ state: MenuState.InvisibleUnmounted,
attributes: { id: 'headlessui-menu-button-1' },
})
- assertMenu({ state: MenuState.Closed })
+ assertMenu({ state: MenuState.InvisibleUnmounted })
// Focus the button
getMenuButton()?.focus()
@@ -1375,10 +1395,10 @@ describe('Keyboard interactions', () => {
)
assertMenuButton({
- state: MenuState.Closed,
+ state: MenuState.InvisibleUnmounted,
attributes: { id: 'headlessui-menu-button-1' },
})
- assertMenu({ state: MenuState.Closed })
+ assertMenu({ state: MenuState.InvisibleUnmounted })
// Focus the button
getMenuButton()?.focus()
@@ -1411,10 +1431,10 @@ describe('Keyboard interactions', () => {
)
assertMenuButton({
- state: MenuState.Closed,
+ state: MenuState.InvisibleUnmounted,
attributes: { id: 'headlessui-menu-button-1' },
})
- assertMenu({ state: MenuState.Closed })
+ assertMenu({ state: MenuState.InvisibleUnmounted })
// Focus the button
getMenuButton()?.focus()
@@ -1423,9 +1443,9 @@ describe('Keyboard interactions', () => {
await press(Keys.ArrowUp)
// Verify it is open
- assertMenuButton({ state: MenuState.Open })
+ assertMenuButton({ state: MenuState.Visible })
assertMenu({
- state: MenuState.Open,
+ state: MenuState.Visible,
attributes: { id: 'headlessui-menu-items-2' },
})
assertMenuButtonLinkedWithMenu()
@@ -1455,10 +1475,10 @@ describe('Keyboard interactions', () => {
)
assertMenuButton({
- state: MenuState.Closed,
+ state: MenuState.InvisibleUnmounted,
attributes: { id: 'headlessui-menu-button-1' },
})
- assertMenu({ state: MenuState.Closed })
+ assertMenu({ state: MenuState.InvisibleUnmounted })
// Focus the button
getMenuButton()?.focus()
@@ -1468,10 +1488,10 @@ describe('Keyboard interactions', () => {
// Verify it is still closed
assertMenuButton({
- state: MenuState.Closed,
+ state: MenuState.InvisibleUnmounted,
attributes: { id: 'headlessui-menu-button-1' },
})
- assertMenu({ state: MenuState.Closed })
+ assertMenu({ state: MenuState.InvisibleUnmounted })
})
)
@@ -1485,14 +1505,14 @@ describe('Keyboard interactions', () => {
)
- assertMenu({ state: MenuState.Closed })
+ assertMenu({ state: MenuState.InvisibleUnmounted })
// Focus the button
getMenuButton()?.focus()
// Open menu
await press(Keys.ArrowUp)
- assertMenu({ state: MenuState.Open })
+ assertMenu({ state: MenuState.Visible })
assertNoActiveMenuItem()
})
@@ -1517,10 +1537,10 @@ describe('Keyboard interactions', () => {
)
assertMenuButton({
- state: MenuState.Closed,
+ state: MenuState.InvisibleUnmounted,
attributes: { id: 'headlessui-menu-button-1' },
})
- assertMenu({ state: MenuState.Closed })
+ assertMenu({ state: MenuState.InvisibleUnmounted })
// Focus the button
getMenuButton()?.focus()
@@ -1555,10 +1575,10 @@ describe('Keyboard interactions', () => {
)
assertMenuButton({
- state: MenuState.Closed,
+ state: MenuState.InvisibleUnmounted,
attributes: { id: 'headlessui-menu-button-1' },
})
- assertMenu({ state: MenuState.Closed })
+ assertMenu({ state: MenuState.InvisibleUnmounted })
// Focus the button
getMenuButton()?.focus()
@@ -1597,10 +1617,10 @@ describe('Keyboard interactions', () => {
)
assertMenuButton({
- state: MenuState.Closed,
+ state: MenuState.InvisibleUnmounted,
attributes: { id: 'headlessui-menu-button-1' },
})
- assertMenu({ state: MenuState.Closed })
+ assertMenu({ state: MenuState.InvisibleUnmounted })
// Focus the button
getMenuButton()?.focus()
@@ -1609,9 +1629,9 @@ describe('Keyboard interactions', () => {
await press(Keys.ArrowUp)
// Verify it is open
- assertMenuButton({ state: MenuState.Open })
+ assertMenuButton({ state: MenuState.Visible })
assertMenu({
- state: MenuState.Open,
+ state: MenuState.Visible,
attributes: { id: 'headlessui-menu-items-2' },
})
assertMenuButtonLinkedWithMenu()
@@ -2360,18 +2380,18 @@ describe('Mouse interactions', () => {
)
assertMenuButton({
- state: MenuState.Closed,
+ state: MenuState.InvisibleUnmounted,
attributes: { id: 'headlessui-menu-button-1' },
})
- assertMenu({ state: MenuState.Closed })
+ assertMenu({ state: MenuState.InvisibleUnmounted })
// Open menu
await click(getMenuButton())
// Verify it is open
- assertMenuButton({ state: MenuState.Open })
+ assertMenuButton({ state: MenuState.Visible })
assertMenu({
- state: MenuState.Open,
+ state: MenuState.Visible,
attributes: { id: 'headlessui-menu-items-2' },
})
assertMenuButtonLinkedWithMenu()
@@ -2398,20 +2418,20 @@ describe('Mouse interactions', () => {
)
assertMenuButton({
- state: MenuState.Closed,
+ state: MenuState.InvisibleUnmounted,
attributes: { id: 'headlessui-menu-button-1' },
})
- assertMenu({ state: MenuState.Closed })
+ assertMenu({ state: MenuState.InvisibleUnmounted })
// Try to open the menu
await click(getMenuButton())
// Verify it is still closed
assertMenuButton({
- state: MenuState.Closed,
+ state: MenuState.InvisibleUnmounted,
attributes: { id: 'headlessui-menu-button-1' },
})
- assertMenu({ state: MenuState.Closed })
+ assertMenu({ state: MenuState.InvisibleUnmounted })
})
)
@@ -2433,14 +2453,14 @@ describe('Mouse interactions', () => {
await click(getMenuButton())
// Verify it is open
- assertMenuButton({ state: MenuState.Open })
+ assertMenuButton({ state: MenuState.Visible })
// Click to close
await click(getMenuButton())
// Verify it is closed
- assertMenuButton({ state: MenuState.Closed })
- assertMenu({ state: MenuState.Closed })
+ assertMenuButton({ state: MenuState.InvisibleUnmounted })
+ assertMenu({ state: MenuState.InvisibleUnmounted })
})
)
@@ -2484,13 +2504,13 @@ describe('Mouse interactions', () => {
)
// Verify that the window is closed
- assertMenu({ state: MenuState.Closed })
+ assertMenu({ state: MenuState.InvisibleUnmounted })
// Click something that is not related to the menu
await click(document.body)
// Should still be closed
- assertMenu({ state: MenuState.Closed })
+ assertMenu({ state: MenuState.InvisibleUnmounted })
})
)
@@ -2510,13 +2530,13 @@ describe('Mouse interactions', () => {
// Open menu
await click(getMenuButton())
- assertMenu({ state: MenuState.Open })
+ assertMenu({ state: MenuState.Visible })
// Click something that is not related to the menu
await click(document.body)
// Should be closed now
- assertMenu({ state: MenuState.Closed })
+ assertMenu({ state: MenuState.InvisibleUnmounted })
// Verify the button is focused again
assertActiveElement(getMenuButton())
@@ -2539,13 +2559,13 @@ describe('Mouse interactions', () => {
// Open menu
await click(getMenuButton())
- assertMenu({ state: MenuState.Open })
+ assertMenu({ state: MenuState.Visible })
// Click the menu button again
await click(getMenuButton())
// Should be closed now
- assertMenu({ state: MenuState.Closed })
+ assertMenu({ state: MenuState.InvisibleUnmounted })
// Verify the button is focused again
assertActiveElement(getMenuButton())
@@ -2828,14 +2848,14 @@ describe('Mouse interactions', () => {
// Open menu
await click(getMenuButton())
- assertMenu({ state: MenuState.Open })
+ assertMenu({ state: MenuState.Visible })
const items = getMenuItems()
// We should be able to click the first item
await click(items[1])
- assertMenu({ state: MenuState.Closed })
+ assertMenu({ state: MenuState.InvisibleUnmounted })
expect(clickHandler).toHaveBeenCalled()
})
)
@@ -2861,11 +2881,11 @@ describe('Mouse interactions', () => {
// Open menu
await click(getMenuButton())
- assertMenu({ state: MenuState.Open })
+ assertMenu({ state: MenuState.Visible })
// We should be able to click the first item
await click(getMenuItems()[1])
- assertMenu({ state: MenuState.Closed })
+ assertMenu({ state: MenuState.InvisibleUnmounted })
// Verify the callback has been called
expect(clickHandler).toHaveBeenCalledTimes(1)
@@ -2875,7 +2895,7 @@ describe('Mouse interactions', () => {
// Click the last item, which should close and invoke the handler
await click(getMenuItems()[2])
- assertMenu({ state: MenuState.Closed })
+ assertMenu({ state: MenuState.InvisibleUnmounted })
// Verify the callback has been called
expect(clickHandler).toHaveBeenCalledTimes(2)
@@ -2900,13 +2920,13 @@ describe('Mouse interactions', () => {
// Open menu
await click(getMenuButton())
- assertMenu({ state: MenuState.Open })
+ assertMenu({ state: MenuState.Visible })
const items = getMenuItems()
// We should be able to click the first item
await click(items[1])
- assertMenu({ state: MenuState.Open })
+ assertMenu({ state: MenuState.Visible })
})
)
@@ -2926,7 +2946,7 @@ describe('Mouse interactions', () => {
// Open menu
await click(getMenuButton())
- assertMenu({ state: MenuState.Open })
+ assertMenu({ state: MenuState.Visible })
const items = getMenuItems()
@@ -2957,7 +2977,7 @@ describe('Mouse interactions', () => {
// Open menu
await click(getMenuButton())
- assertMenu({ state: MenuState.Open })
+ assertMenu({ state: MenuState.Visible })
const items = getMenuItems()
@@ -2991,7 +3011,7 @@ describe('Mouse interactions', () => {
// Open menu
await click(getMenuButton())
- assertMenu({ state: MenuState.Open })
+ assertMenu({ state: MenuState.Visible })
const items = getMenuItems()
diff --git a/packages/@headlessui-react/src/components/menu/menu.tsx b/packages/@headlessui-react/src/components/menu/menu.tsx
index a2a9948..2fcc84c 100644
--- a/packages/@headlessui-react/src/components/menu/menu.tsx
+++ b/packages/@headlessui-react/src/components/menu/menu.tsx
@@ -3,7 +3,7 @@ import * as React from 'react'
import { Props } from '../../types'
import { match } from '../../utils/match'
-import { forwardRefWithAs, render } from '../../utils/render'
+import { forwardRefWithAs, render, Features, PropsForFeatures } from '../../utils/render'
import { disposables } from '../../utils/disposables'
import { useDisposables } from '../../hooks/use-disposables'
import { useIsoMorphicEffect } from '../../hooks/use-iso-morphic-effect'
@@ -108,7 +108,11 @@ const reducers: {
action: Extract
) => StateDefinition
} = {
- [ActionTypes.CloseMenu]: state => ({ ...state, menuState: MenuStates.Closed }),
+ [ActionTypes.CloseMenu]: state => ({
+ ...state,
+ activeItemIndex: null,
+ menuState: MenuStates.Closed,
+ }),
[ActionTypes.OpenMenu]: state => ({ ...state, menuState: MenuStates.Open }),
[ActionTypes.GoToItem]: (state, action) => {
const activeItemIndex = calculateActiveItemIndex(state, action.focus, action.id)
@@ -348,14 +352,15 @@ type ItemsPropsWeControl =
const DEFAULT_ITEMS_TAG = 'div'
type ItemsRenderPropArg = { open: boolean }
+const ItemsRenderFeatures = Features.RenderStrategy | Features.Static
const Items = forwardRefWithAs(function Items<
TTag extends React.ElementType = typeof DEFAULT_ITEMS_TAG
>(
- props: Props & { static?: boolean },
+ props: Props &
+ PropsForFeatures,
ref: React.Ref
) {
- const { static: isStatic = false, ...passthroughProps } = props
const [state, dispatch] = useMenuContext([Menu.name, Items.name].join('.'))
const itemsRef = useSyncRefs(state.itemsRef, ref)
@@ -433,14 +438,16 @@ const Items = forwardRefWithAs(function Items<
onKeyDown: handleKeyDown,
role: 'menu',
tabIndex: 0,
+ ref: itemsRef,
}
-
- if (!isStatic && state.menuState === MenuStates.Closed) return null
+ const passthroughProps = props
return render(
- { ...passthroughProps, ...propsWeControl, ...{ ref: itemsRef } },
+ { ...passthroughProps, ...propsWeControl },
propsBag,
- DEFAULT_ITEMS_TAG
+ DEFAULT_ITEMS_TAG,
+ ItemsRenderFeatures,
+ state.menuState === MenuStates.Open
)
})
@@ -529,11 +536,7 @@ function Item(
onPointerLeave: handlePointerLeave,
}
- return render(
- { ...passthroughProps, ...propsWeControl },
- propsBag,
- DEFAULT_ITEM_TAG
- )
+ return render({ ...passthroughProps, ...propsWeControl }, propsBag, DEFAULT_ITEM_TAG)
}
function resolvePropValue(property: TProperty, bag: TBag) {
diff --git a/packages/@headlessui-react/src/components/transitions/transition.test.tsx b/packages/@headlessui-react/src/components/transitions/transition.test.tsx
index 6acb8a5..79a74a1 100644
--- a/packages/@headlessui-react/src/components/transitions/transition.test.tsx
+++ b/packages/@headlessui-react/src/components/transitions/transition.test.tsx
@@ -120,7 +120,9 @@ describe('Setup API', () => {
it('should be possible to use a render prop', () => {
const { container } = render(
- {ref => Children }
+
+ {() => Children }
+
)
expect(container.firstChild).toMatchInlineSnapshot(`
@@ -131,12 +133,20 @@ describe('Setup API', () => {
})
it(
- 'should yell at us when we forget to apply the ref when using a render prop',
+ 'should yell at us when we forget to forward the ref when using a render prop',
suppressConsoleLogs(() => {
expect.assertions(1)
+ function Dummy(props: any) {
+ return Children
+ }
+
expect(() => {
- render({() => Children } )
+ render(
+
+ {() => }
+
+ )
}).toThrowErrorMatchingInlineSnapshot(
`"Did you forget to passthrough the \`ref\` to the actual DOM node?"`
)
@@ -253,8 +263,10 @@ describe('Setup API', () => {
const { container } = render(
- {ref => }
- {ref => }
+ {() => }
+
+ {() => }
+
)
@@ -278,11 +290,15 @@ describe('Setup API', () => {
it('should be possible to use render props on the Transition and Transition.Child components', () => {
const { container } = render(
-
- {ref => (
-
- {ref => }
- {ref => }
+
+ {() => (
+
+
+ {() => }
+
+
+ {() => }
+
)}
@@ -306,16 +322,24 @@ describe('Setup API', () => {
})
it(
- 'should yell at us when we forgot to apply the ref on one of the Transition.Child components',
+ 'should yell at us when we forgot to forward the ref on one of the Transition.Child components',
suppressConsoleLogs(() => {
expect.assertions(1)
+ function Dummy(props: any) {
+ return
+ }
+
expect(() => {
render(
- {ref => }
- {() => }
+
+ {() => Sidebar }
+
+
+ {() => Content }
+
)
@@ -326,21 +350,23 @@ describe('Setup API', () => {
)
it(
- 'should yell at us when we forgot to apply a ref on the Transition component',
+ 'should yell at us when we forgot to forward a ref on the Transition component',
suppressConsoleLogs(() => {
expect.assertions(1)
+ function Dummy(props: any) {
+ return
+ }
+
expect(() => {
render(
-
+
{() => (
-
- {ref => }
-
- {ref => }
-
-
+
+ {() => }
+ {() => }
+
)}
@@ -503,6 +529,53 @@ describe('Transitions', () => {
`)
})
+ it('should transition in completely (duration defined in seconds) in (render strategy = hidden)', async () => {
+ const enterDuration = 50
+
+ function Example() {
+ const [show, setShow] = React.useState(false)
+
+ return (
+ <>
+
+
+
+ Hello!
+
+
+ setShow(v => !v)}>
+ Toggle
+
+ >
+ )
+ }
+
+ const timeline = await executeTimeline( , [
+ // Toggle to show
+ ({ getByTestId }) => {
+ fireEvent.click(getByTestId('toggle'))
+ return executeTimeline.fullTransition(enterDuration)
+ },
+ ])
+
+ expect(timeline).toMatchInlineSnapshot(`
+ "Render 1:
+ - hidden=\\"\\"
+ - style=\\"display: none;\\"
+ + class=\\"enter from\\"
+ + style=\\"\\"
+
+ Render 2:
+ - class=\\"enter from\\"
+ + class=\\"enter to\\"
+
+ Render 3: Transition took at least 50ms (yes)
+ - class=\\"enter to\\"
+ + class=\\"\\""
+ `)
+ })
+
it('should transition in completely', async () => {
const enterDuration = 50
@@ -606,6 +679,57 @@ describe('Transitions', () => {
})
)
+ it(
+ 'should transition out completely (render strategy = hidden)',
+ suppressConsoleLogs(async () => {
+ const leaveDuration = 50
+
+ function Example() {
+ const [show, setShow] = React.useState(true)
+
+ return (
+ <>
+
+
+
+ Hello!
+
+
+ setShow(v => !v)}>
+ Toggle
+
+ >
+ )
+ }
+
+ const timeline = await executeTimeline( , [
+ // Toggle to hide
+ ({ getByTestId }) => {
+ fireEvent.click(getByTestId('toggle'))
+ return executeTimeline.fullTransition(leaveDuration)
+ },
+ ])
+
+ expect(timeline).toMatchInlineSnapshot(`
+ "Render 1:
+ -
+ +
+
+ Render 2:
+ - class=\\"leave from\\"
+ + class=\\"leave to\\"
+
+ Render 3: Transition took at least 50ms (yes)
+ - class=\\"leave to\\"
+ + class=\\"\\"
+ + hidden=\\"\\"
+ + style=\\"display: none;\\""
+ `)
+ })
+ )
+
it(
'should transition in and out completely',
suppressConsoleLogs(async () => {
@@ -690,6 +814,108 @@ describe('Transitions', () => {
`)
})
)
+
+ it(
+ 'should transition in and out completely (render strategy = hidden)',
+ suppressConsoleLogs(async () => {
+ const enterDuration = 50
+ const leaveDuration = 75
+
+ function Example() {
+ const [show, setShow] = React.useState(false)
+
+ return (
+ <>
+
+
+
+
+ Hello!
+
+
+
setShow(v => !v)}>
+ Toggle
+
+ >
+ )
+ }
+
+ const timeline = await executeTimeline(
, [
+ // Toggle to show
+ ({ getByTestId }) => {
+ fireEvent.click(getByTestId('toggle'))
+ return executeTimeline.fullTransition(enterDuration)
+ },
+
+ // Toggle to hide
+ ({ getByTestId }) => {
+ fireEvent.click(getByTestId('toggle'))
+ return executeTimeline.fullTransition(leaveDuration)
+ },
+
+ // Toggle to show
+ ({ getByTestId }) => {
+ fireEvent.click(getByTestId('toggle'))
+ return executeTimeline.fullTransition(leaveDuration)
+ },
+ ])
+
+ expect(timeline).toMatchInlineSnapshot(`
+ "Render 1:
+ - hidden=\\"\\"
+ - style=\\"display: none;\\"
+ + class=\\"enter enter-from\\"
+ + style=\\"\\"
+
+ Render 2:
+ - class=\\"enter enter-from\\"
+ + class=\\"enter enter-to\\"
+
+ Render 3: Transition took at least 50ms (yes)
+ - class=\\"enter enter-to\\"
+ + class=\\"\\"
+
+ Render 4:
+ - class=\\"\\"
+ + class=\\"leave leave-from\\"
+
+ Render 5:
+ - class=\\"leave leave-from\\"
+ + class=\\"leave leave-to\\"
+
+ Render 6: Transition took at least 75ms (yes)
+ - class=\\"leave leave-to\\"
+ - style=\\"\\"
+ + class=\\"\\"
+ + hidden=\\"\\"
+ + style=\\"display: none;\\"
+
+ Render 7:
+ - class=\\"\\"
+ - hidden=\\"\\"
+ - style=\\"display: none;\\"
+ + class=\\"enter enter-from\\"
+ + style=\\"\\"
+
+ Render 8:
+ - class=\\"enter enter-from\\"
+ + class=\\"enter enter-to\\"
+
+ Render 9: Transition took at least 75ms (yes)
+ - class=\\"enter enter-to\\"
+ + class=\\"\\""
+ `)
+ })
+ )
})
describe('nested transitions', () => {
diff --git a/packages/@headlessui-react/src/components/transitions/transition.tsx b/packages/@headlessui-react/src/components/transitions/transition.tsx
index 37c035c..121e6d2 100644
--- a/packages/@headlessui-react/src/components/transitions/transition.tsx
+++ b/packages/@headlessui-react/src/components/transitions/transition.tsx
@@ -1,11 +1,13 @@
import * as React from 'react'
+import { Props } from 'types'
import { useId } from '../../hooks/use-id'
import { useIsInitialRender } from '../../hooks/use-is-initial-render'
+import { match } from '../../utils/match'
import { useIsMounted } from '../../hooks/use-is-mounted'
import { useIsoMorphicEffect } from '../../hooks/use-iso-morphic-effect'
-import { match } from '../../utils/match'
+import { Features, PropsForFeatures, render, RenderStrategy } from '../../utils/render'
import { Reason, transition } from './utils/transition'
type ID = ReturnType
@@ -43,24 +45,9 @@ export type TransitionEvents = Partial<{
afterLeave(): void
}>
-type HTMLTags = keyof JSX.IntrinsicElements
-type HTMLTagProps = JSX.IntrinsicElements[TTag]
-
-type AsShortcut = {
- children?: React.ReactNode
- as?: TTag
-} & Omit, 'ref'>
-
-type AsRenderPropFunction = {
- children: (ref: React.MutableRefObject) => JSX.Element
-}
-
-type BaseConfig = Partial<{ appear: boolean }>
-
-type TransitionChildProps = BaseConfig &
- (AsShortcut | AsRenderPropFunction) &
- TransitionClasses &
- TransitionEvents
+type TransitionChildProps = Props &
+ PropsForFeatures &
+ Partial<{ appear: boolean } & TransitionClasses & TransitionEvents>
function useTransitionContext() {
const context = React.useContext(TransitionContext)
@@ -83,16 +70,23 @@ function useParentNesting() {
}
type NestingContextValues = {
- children: React.MutableRefObject
+ children: React.MutableRefObject<{ id: ID; state: TreeStates }[]>
register: (id: ID) => () => void
- unregister: (id: ID) => void
+ unregister: (id: ID, strategy?: RenderStrategy) => void
}
const NestingContext = React.createContext(null)
+function hasChildren(
+ bag: NestingContextValues['children'] | { children: NestingContextValues['children'] }
+): boolean {
+ if ('children' in bag) return hasChildren(bag.children)
+ return bag.current.filter(({ state }) => state === TreeStates.Visible).length > 0
+}
+
function useNesting(done?: () => void) {
const doneRef = React.useRef(done)
- const transitionableChildren = React.useRef([])
+ const transitionableChildren = React.useRef([])
const mounted = useIsMounted()
React.useEffect(() => {
@@ -100,14 +94,20 @@ function useNesting(done?: () => void) {
}, [done])
const unregister = React.useCallback(
- (childId: ID) => {
- const idx = transitionableChildren.current.indexOf(childId)
-
+ (childId: ID, strategy = RenderStrategy.Hidden) => {
+ const idx = transitionableChildren.current.findIndex(({ id }) => id === childId)
if (idx === -1) return
- transitionableChildren.current.splice(idx, 1)
+ match(strategy, {
+ [RenderStrategy.Unmount]() {
+ transitionableChildren.current.splice(idx, 1)
+ },
+ [RenderStrategy.Hidden]() {
+ transitionableChildren.current[idx].state = TreeStates.Hidden
+ },
+ })
- if (transitionableChildren.current.length <= 0 && mounted.current) {
+ if (!hasChildren(transitionableChildren) && mounted.current) {
doneRef.current?.()
}
},
@@ -116,8 +116,14 @@ function useNesting(done?: () => void) {
const register = React.useCallback(
(childId: ID) => {
- transitionableChildren.current.push(childId)
- return () => unregister(childId)
+ const child = transitionableChildren.current.find(({ id }) => id === childId)
+ if (!child) {
+ transitionableChildren.current.push({ id: childId, state: TreeStates.Visible })
+ } else if (child.state !== TreeStates.Visible) {
+ child.state = TreeStates.Visible
+ }
+
+ return () => unregister(childId, RenderStrategy.Unmount)
},
[transitionableChildren, unregister]
)
@@ -156,7 +162,15 @@ function useEvents(events: TransitionEvents) {
return eventsRef
}
-function TransitionChild(props: TransitionChildProps) {
+// ---
+
+const DEFAULT_TRANSITION_CHILD_TAG = 'div'
+type TransitionChildRenderPropArg = React.MutableRefObject
+const TransitionChildRenderFeatures = Features.RenderStrategy
+
+function TransitionChild(
+ props: TransitionChildProps
+) {
const {
// Event "handlers"
beforeEnter,
@@ -171,13 +185,11 @@ function TransitionChild(props: TransitionChildPr
leave,
leaveFrom,
leaveTo,
-
- // ..
- children,
...rest
} = props
const container = React.useRef(null)
const [state, setState] = React.useState(TreeStates.Visible)
+ const strategy = rest.unmount ? RenderStrategy.Unmount : RenderStrategy.Hidden
const { show, appear } = useTransitionContext()
const { register, unregister } = useParentNesting()
@@ -202,6 +214,23 @@ function TransitionChild(props: TransitionChildPr
return register(id)
}, [register, id])
+ useIsoMorphicEffect(() => {
+ // If we are in another mode than the Hidden mode then ignore
+ if (strategy !== RenderStrategy.Hidden) return
+ if (!id) return
+
+ // Make sure that we are visible
+ if (show && state !== TreeStates.Visible) {
+ setState(TreeStates.Visible)
+ return
+ }
+
+ match(state, {
+ [TreeStates.Hidden]: () => unregister(id),
+ [TreeStates.Visible]: () => register(id),
+ })
+ }, [state, id, register, unregister, show, strategy])
+
const enterClasses = useSplitClasses(enter)
const enterFromClasses = useSplitClasses(enterFrom)
const enterToClasses = useSplitClasses(enterTo)
@@ -243,7 +272,7 @@ function TransitionChild(props: TransitionChildPr
// When we don't have children anymore we can safely unregister from the parent and hide
// ourselves.
- if (nesting.children.current.length <= 0) {
+ if (!hasChildren(nesting)) {
setState(TreeStates.Hidden)
unregister(id)
events.current.afterLeave()
@@ -266,32 +295,27 @@ function TransitionChild(props: TransitionChildPr
leaveToClasses,
])
- // Unmount the whole tree
- if (state === TreeStates.Hidden) return null
+ const propsBag = {}
+ const propsWeControl = { ref: container }
+ const passthroughProps = rest
- if (typeof children === 'function') {
- return (
-
- {(children as AsRenderPropFunction['children'])(container)}
-
- )
- }
-
- const { as: Component = 'div', ...passthroughProps } = rest as AsShortcut
return (
- {/* @ts-expect-error Expression produces a union type that is too complex to represent. */}
-
- {children}
-
+ {render(
+ { ...passthroughProps, ...propsWeControl },
+ propsBag,
+ DEFAULT_TRANSITION_CHILD_TAG,
+ TransitionChildRenderFeatures,
+ state === TreeStates.Visible
+ )}
)
}
-export function Transition(
+export function Transition(
props: TransitionChildProps & { show: boolean; appear?: boolean }
) {
- const { show, appear = false, ...rest } = props
+ const { show, appear = false, unmount, ...passthroughProps } = props
if (![true, false].includes(show)) {
throw new Error('A is used but it is missing a `show={true | false}` prop.')
@@ -312,18 +336,28 @@ export function Transition(
React.useEffect(() => {
if (show) {
setState(TreeStates.Visible)
- } else if (nestingBag.children.current.length <= 0) {
+ } else if (!hasChildren(nestingBag)) {
setState(TreeStates.Hidden)
}
}, [show, nestingBag])
+ const sharedProps = { unmount }
+ const propsBag = {}
+
return (
- {match(state, {
- [TreeStates.Visible]: () => ,
- [TreeStates.Hidden]: null,
- })}
+ {render(
+ {
+ ...sharedProps,
+ as: React.Fragment,
+ children: ,
+ },
+ propsBag,
+ React.Fragment,
+ TransitionChildRenderFeatures,
+ state === TreeStates.Visible
+ )}
)
diff --git a/packages/@headlessui-react/src/test-utils/accessibility-assertions.ts b/packages/@headlessui-react/src/test-utils/accessibility-assertions.ts
index dd45b20..d70f60f 100644
--- a/packages/@headlessui-react/src/test-utils/accessibility-assertions.ts
+++ b/packages/@headlessui-react/src/test-utils/accessibility-assertions.ts
@@ -27,8 +27,14 @@ export function getMenuItems(): HTMLElement[] {
// ---
export enum MenuState {
- Open,
- Closed,
+ /** The menu is visible to the user. */
+ Visible,
+
+ /** The menu is **not** visible to the user. It's still in the DOM, but it is hidden. */
+ InvisibleHidden,
+
+ /** The menu is **not** visible to the user. It's not in the DOM, it is unmounted. */
+ InvisibleUnmounted,
}
export function assertMenuButton(
@@ -47,12 +53,17 @@ export function assertMenuButton(
expect(button).toHaveAttribute('aria-haspopup')
switch (options.state) {
- case MenuState.Open:
+ case MenuState.Visible:
expect(button).toHaveAttribute('aria-controls')
expect(button).toHaveAttribute('aria-expanded', 'true')
break
- case MenuState.Closed:
+ case MenuState.InvisibleHidden:
+ expect(button).toHaveAttribute('aria-controls')
+ expect(button).not.toHaveAttribute('aria-expanded')
+ break
+
+ case MenuState.InvisibleUnmounted:
expect(button).not.toHaveAttribute('aria-controls')
expect(button).not.toHaveAttribute('aria-expanded')
break
@@ -124,27 +135,37 @@ export function assertMenu(
) {
try {
switch (options.state) {
- case MenuState.Open:
+ case MenuState.InvisibleHidden:
if (menu === null) return expect(menu).not.toBe(null)
- // Check that some attributes exists, doesn't really matter what the values are at this point in
- // time, we just require them.
- expect(menu).toHaveAttribute('aria-labelledby')
+ assertHidden(menu)
- // Check that we have the correct values for certain attributes
+ expect(menu).toHaveAttribute('aria-labelledby')
expect(menu).toHaveAttribute('role', 'menu')
- if (options.textContent) {
- expect(menu).toHaveTextContent(options.textContent)
- }
+ if (options.textContent) expect(menu).toHaveTextContent(options.textContent)
- // Ensure menu button has the following attributes
for (let attributeName in options.attributes) {
expect(menu).toHaveAttribute(attributeName, options.attributes[attributeName])
}
break
- case MenuState.Closed:
+ case MenuState.Visible:
+ if (menu === null) return expect(menu).not.toBe(null)
+
+ assertVisible(menu)
+
+ expect(menu).toHaveAttribute('aria-labelledby')
+ expect(menu).toHaveAttribute('role', 'menu')
+
+ if (options.textContent) expect(menu).toHaveTextContent(options.textContent)
+
+ for (let attributeName in options.attributes) {
+ expect(menu).toHaveAttribute(attributeName, options.attributes[attributeName])
+ }
+ break
+
+ case MenuState.InvisibleUnmounted:
expect(menu).toBe(null)
break
@@ -217,8 +238,14 @@ export function getListboxOptions(): HTMLElement[] {
// ---
export enum ListboxState {
- Open,
- Closed,
+ /** The listbox is visible to the user. */
+ Visible,
+
+ /** The listbox is **not** visible to the user. It's still in the DOM, but it is hidden. */
+ InvisibleHidden,
+
+ /** The listbox is **not** visible to the user. It's not in the DOM, it is unmounted. */
+ InvisibleUnmounted,
}
export function assertListbox(
@@ -231,27 +258,37 @@ export function assertListbox(
) {
try {
switch (options.state) {
- case ListboxState.Open:
+ case ListboxState.InvisibleHidden:
if (listbox === null) return expect(listbox).not.toBe(null)
- // Check that some attributes exists, doesn't really matter what the values are at this point in
- // time, we just require them.
- expect(listbox).toHaveAttribute('aria-labelledby')
+ assertHidden(listbox)
- // Check that we have the correct values for certain attributes
+ expect(listbox).toHaveAttribute('aria-labelledby')
expect(listbox).toHaveAttribute('role', 'listbox')
- if (options.textContent) {
- expect(listbox).toHaveTextContent(options.textContent)
- }
+ if (options.textContent) expect(listbox).toHaveTextContent(options.textContent)
- // Ensure listbox button has the following attributes
for (let attributeName in options.attributes) {
expect(listbox).toHaveAttribute(attributeName, options.attributes[attributeName])
}
break
- case ListboxState.Closed:
+ case ListboxState.Visible:
+ if (listbox === null) return expect(listbox).not.toBe(null)
+
+ assertVisible(listbox)
+
+ expect(listbox).toHaveAttribute('aria-labelledby')
+ expect(listbox).toHaveAttribute('role', 'listbox')
+
+ if (options.textContent) expect(listbox).toHaveTextContent(options.textContent)
+
+ for (let attributeName in options.attributes) {
+ expect(listbox).toHaveAttribute(attributeName, options.attributes[attributeName])
+ }
+ break
+
+ case ListboxState.InvisibleUnmounted:
expect(listbox).toBe(null)
break
@@ -280,12 +317,17 @@ export function assertListboxButton(
expect(button).toHaveAttribute('aria-haspopup')
switch (options.state) {
- case ListboxState.Open:
+ case ListboxState.Visible:
expect(button).toHaveAttribute('aria-controls')
expect(button).toHaveAttribute('aria-expanded', 'true')
break
- case ListboxState.Closed:
+ case ListboxState.InvisibleHidden:
+ expect(button).toHaveAttribute('aria-controls')
+ expect(button).not.toHaveAttribute('aria-expanded')
+ break
+
+ case ListboxState.InvisibleUnmounted:
expect(button).not.toHaveAttribute('aria-controls')
expect(button).not.toHaveAttribute('aria-expanded')
break
@@ -567,3 +609,29 @@ export function assertActiveElement(element: HTMLElement | null) {
throw err
}
}
+
+// ---
+
+export function assertHidden(element: HTMLElement | null) {
+ try {
+ if (element === null) return expect(element).not.toBe(null)
+
+ expect(element).toHaveAttribute('hidden')
+ expect(element).toHaveStyle({ display: 'none' })
+ } catch (err) {
+ Error.captureStackTrace(err, assertHidden)
+ throw err
+ }
+}
+
+export function assertVisible(element: HTMLElement | null) {
+ try {
+ if (element === null) return expect(element).not.toBe(null)
+
+ expect(element).not.toHaveAttribute('hidden')
+ expect(element).not.toHaveStyle({ display: 'none' })
+ } catch (err) {
+ Error.captureStackTrace(err, assertVisible)
+ throw err
+ }
+}
diff --git a/packages/@headlessui-react/src/types.ts b/packages/@headlessui-react/src/types.ts
index d0d32d7..bbb7ba2 100644
--- a/packages/@headlessui-react/src/types.ts
+++ b/packages/@headlessui-react/src/types.ts
@@ -1,8 +1,25 @@
+// A unique placeholder we can use as some defaults. This is nice because we can use this instead of
+// defaulting to null / never / ... and possibly collide with actual data.
+const __: unique symbol = Symbol('__placeholder__')
+export type __ = typeof __
+
export type PropsOf = TTag extends React.ElementType
? React.ComponentProps
: never
-export type Props = {
+export type Props = {
as?: TTag
children?: React.ReactNode | ((bag: TSlot) => React.ReactElement)
-} & Omit, TOmitableProps>
+ refName?: string
+} & (TOmitableProps extends __ ? PropsOf : Omit, TOmitableProps>)
+
+type Without = { [P in Exclude]?: never }
+export type XOR = T | U extends __
+ ? never
+ : T extends __
+ ? U
+ : U extends __
+ ? T
+ : T | U extends object
+ ? (Without & U) | (Without & T)
+ : T | U
diff --git a/packages/@headlessui-react/src/utils/render.test.tsx b/packages/@headlessui-react/src/utils/render.test.tsx
new file mode 100644
index 0000000..3bb895c
--- /dev/null
+++ b/packages/@headlessui-react/src/utils/render.test.tsx
@@ -0,0 +1,406 @@
+import React from 'react'
+import { render as testRender, prettyDOM, getByTestId } from '@testing-library/react'
+
+import { suppressConsoleLogs } from '../test-utils/suppress-console-logs'
+import { render, Features, PropsForFeatures } from './render'
+import { Props } from '../types'
+
+function contents() {
+ return prettyDOM(getByTestId(document.body, 'wrapper'), undefined, {
+ highlight: false,
+ })
+}
+
+describe('Default functionality', () => {
+ const bag = {}
+ function Dummy(
+ props: Props & Partial<{ a: any; b: any; c: any }>
+ ) {
+ return {render(props, bag, 'div')}
+ }
+
+ it('should be possible to render a dummy component', () => {
+ testRender( )
+
+ expect(contents()).toMatchInlineSnapshot(`
+ ""
+ `)
+ })
+
+ it('should be possible to render a dummy component with some children as a callback', () => {
+ expect.assertions(2)
+
+ testRender(
+
+ {data => {
+ expect(data).toBe(bag)
+
+ return Contents
+ }}
+
+ )
+
+ expect(contents()).toMatchInlineSnapshot(`
+ ""
+ `)
+ })
+
+ it('should be possible to add a ref with a different name', () => {
+ const ref = React.createRef()
+
+ function MyComponent({
+ innerRef,
+ ...props
+ }: Props & { innerRef: React.Ref }) {
+ return
+ }
+
+ function OtherDummy(props: Props) {
+ return {render({ ...props, ref }, bag, 'div')}
+ }
+
+ testRender(
+
+ Contents
+
+ )
+
+ expect(contents()).toMatchInlineSnapshot(`
+ ""
+ `)
+ })
+
+ it('should be possible to passthrough props to a dummy component', () => {
+ testRender( )
+
+ expect(contents()).toMatchInlineSnapshot(`
+ ""
+ `)
+ })
+
+ it('should be possible to change the underlying DOM node using the `as` prop', () => {
+ testRender( )
+
+ expect(contents()).toMatchInlineSnapshot(`
+ "
+
+
"
+ `)
+ })
+
+ it('should be possible to change the underlying DOM node using the `as` prop and still have a function as children', () => {
+ testRender({() => Contents } )
+
+ expect(contents()).toMatchInlineSnapshot(`
+ "
+
+
+ Contents
+
+
+
"
+ `)
+ })
+
+ it('should be possible to render the children only when the `as` prop is set to React.Fragment', () => {
+ testRender(Contents )
+
+ expect(contents()).toMatchInlineSnapshot(`
+ "
+ Contents
+
"
+ `)
+ })
+
+ it('should forward all the props to the first child when using an as={React.Fragment}', () => {
+ testRender(
+
+ {() => Contents }
+
+ )
+
+ expect(contents()).toMatchInlineSnapshot(`
+ "
+
+ Contents
+
+
"
+ `)
+ })
+
+ it(
+ 'should error when we are rendering a React.Fragment with multiple children',
+ suppressConsoleLogs(() => {
+ expect.assertions(1)
+
+ return expect(() => {
+ testRender(
+ // @ts-expect-error className cannot be applied to a React.Fragment
+
+ Contents A
+ Contents B
+
+ )
+ }).toThrowErrorMatchingInlineSnapshot(`"You should only render 1 child"`)
+ })
+ )
+
+ it("should not error when we are rendering a React.Fragment with multiple children when we don't passthrough additional props", () => {
+ testRender(
+
+ Contents A
+ Contents B
+
+ )
+
+ expect(contents()).toMatchInlineSnapshot(`
+ "
+
+ Contents A
+
+
+ Contents B
+
+
"
+ `)
+ })
+
+ it(
+ 'should error when we are applying props to a React.Fragment when we do not have a dedicated element',
+ suppressConsoleLogs(() => {
+ expect.assertions(1)
+
+ return expect(() => {
+ testRender(
+ // @ts-expect-error className cannot be applied to a React.Fragment
+
+ Contents
+
+ )
+ }).toThrowErrorMatchingInlineSnapshot(
+ `"You should render an element as a child. Did you forget the as=\\"...\\" prop?"`
+ )
+ })
+ )
+})
+
+// ---
+
+function testStaticFeature(Dummy) {
+ it('should be possible to render a `static` dummy component (show = true)', () => {
+ testRender(
+
+ Contents
+
+ )
+
+ expect(contents()).toMatchInlineSnapshot(`
+ ""
+ `)
+ })
+
+ it('should be possible to render a `static` dummy component (show = false)', () => {
+ testRender(
+
+ Contents
+
+ )
+
+ expect(contents()).toMatchInlineSnapshot(`
+ ""
+ `)
+ })
+}
+
+// With the `static` keyword, the user is always in control. When we internally decide to show the
+// component or hide it then it won't have any effect. This is useful for when you want to wrap your
+// component in a Transition for example so that the Transition component can control the
+// showing/hiding based on the `show` prop AND the state of the transition.
+describe('Features.Static', () => {
+ const bag = {}
+ const EnabledFeatures = Features.Static
+ function Dummy(
+ props: Props & { show: boolean } & PropsForFeatures
+ ) {
+ const { show, ...rest } = props
+ return {render(rest, bag, 'div', EnabledFeatures, show)}
+ }
+
+ testStaticFeature(Dummy)
+})
+
+// ---
+
+function testRenderStrategyFeature(Dummy) {
+ describe('Unmount render strategy', () => {
+ it('should be possible to render an `unmount` dummy component (show = true)', () => {
+ testRender(
+
+ Contents
+
+ )
+
+ expect(contents()).toMatchInlineSnapshot(`
+ ""
+ `)
+ })
+
+ it('should be possible to render an `unmount` dummy component (show = false)', () => {
+ testRender(
+
+ Contents
+
+ )
+
+ // No contents, because we unmounted!
+ expect(contents()).toMatchInlineSnapshot(`
+ "
"
+ `)
+ })
+ })
+
+ describe('Hidden render strategy', () => {
+ it('should be possible to render an `unmount={false}` dummy component (show = true)', () => {
+ testRender(
+
+ Contents
+
+ )
+
+ expect(contents()).toMatchInlineSnapshot(`
+ ""
+ `)
+ })
+
+ it('should be possible to render an `unmount={false}` dummy component (show = false)', () => {
+ testRender(
+
+ Contents
+
+ )
+
+ // We do have contents, but it is marked as hidden!
+ expect(contents()).toMatchInlineSnapshot(`
+ ""
+ `)
+ })
+ })
+}
+
+describe('Features.RenderStrategy', () => {
+ const bag = {}
+ const EnabledFeatures = Features.RenderStrategy
+ function Dummy(
+ props: Props & { show: boolean } & PropsForFeatures
+ ) {
+ const { show, ...rest } = props
+ return {render(rest, bag, 'div', EnabledFeatures, show)}
+ }
+
+ testRenderStrategyFeature(Dummy)
+})
+
+// ---
+
+// This should enable the `static` and `unmount` features. However they can't be used together!
+describe('Features.Static | Features.RenderStrategy', () => {
+ const bag = {}
+ const EnabledFeatures = Features.Static | Features.RenderStrategy
+ function Dummy(
+ props: Props & { show: boolean } & PropsForFeatures
+ ) {
+ const { show, ...rest } = props
+ return {render(rest, bag, 'div', EnabledFeatures, show)}
+ }
+
+ // TODO: Can we "legit" test this? 🤔
+ it('should result in a typescript error', () => {
+ testRender(
+ // @ts-expect-error static & unmount together are incompatible
+
+ Contents
+
+ )
+ })
+
+ // To avoid duplication, and to make sure that the features tested in isolation can also be
+ // re-used when they are combined.
+ testStaticFeature(Dummy)
+ testRenderStrategyFeature(Dummy)
+})
diff --git a/packages/@headlessui-react/src/utils/render.ts b/packages/@headlessui-react/src/utils/render.ts
index 0c73cab..a255c0d 100644
--- a/packages/@headlessui-react/src/utils/render.ts
+++ b/packages/@headlessui-react/src/utils/render.ts
@@ -1,12 +1,94 @@
import * as React from 'react'
-import { Props } from '../types'
+import { Props, XOR, __ } from '../types'
+import { match } from './match'
-export function render(
- props: Props,
+export enum Features {
+ /** No features at all */
+ None = 0,
+
+ /**
+ * When used, this will allow us to use one of the render strategies.
+ *
+ * **The render strategies are:**
+ * - **Unmount** _(Will unmount the component.)_
+ * - **Hidden** _(Will hide the component using the [hidden] attribute.)_
+ */
+ RenderStrategy = 1,
+
+ /**
+ * When used, this will allow the user of our component to be in control. This can be used when
+ * you want to transition based on some state.
+ */
+ Static = 2,
+}
+
+export enum RenderStrategy {
+ Unmount,
+ Hidden,
+}
+
+type PropsForFeature = {
+ [P in TPassedInFeatures]: P extends TForFeature ? TProps : __
+}[TPassedInFeatures]
+
+export type PropsForFeatures = XOR<
+ PropsForFeature,
+ PropsForFeature
+>
+
+export function render(
+ props: Props & PropsForFeatures,
+ propsBag: TBag,
+ defaultTag: React.ElementType,
+ features?: TFeature,
+ visible: boolean = true
+) {
+ // Visible always render
+ if (visible) return _render(props, propsBag, defaultTag)
+
+ const featureFlags = features ?? Features.None
+
+ if (featureFlags & Features.Static) {
+ const { static: isStatic = false, ...rest } = props as PropsForFeatures
+
+ // When the `static` prop is passed as `true`, then the user is in control, thus we don't care about anything else
+ if (isStatic) return _render(rest, propsBag, defaultTag)
+ }
+
+ if (featureFlags & Features.RenderStrategy) {
+ const { unmount = true, ...rest } = props as PropsForFeatures
+ const strategy = unmount ? RenderStrategy.Unmount : RenderStrategy.Hidden
+
+ return match(strategy, {
+ [RenderStrategy.Unmount]() {
+ return null
+ },
+ [RenderStrategy.Hidden]() {
+ return _render(
+ { ...rest, ...{ hidden: true, style: { display: 'none' } } },
+ propsBag,
+ defaultTag
+ )
+ },
+ })
+ }
+
+ // No features enabled, just render
+ return _render(props, propsBag, defaultTag)
+}
+
+function _render(
+ props: Props & { ref?: unknown },
bag: TBag,
tag: React.ElementType
) {
- const { as: Component = tag, children, ...passThroughProps } = props
+ const { as: Component = tag, children, refName = 'ref', ...passThroughProps } = omit(props, [
+ 'unmount',
+ 'static',
+ ])
+
+ // This allows us to use ` `
+ const refRelatedProps = props.ref !== undefined ? { [refName]: props.ref } : {}
const resolvedChildren = (typeof children === 'function' ? children(bag) : children) as
| React.ReactElement
@@ -16,7 +98,7 @@ export function render(
if (Object.keys(passThroughProps).length > 0) {
if (Array.isArray(resolvedChildren) && resolvedChildren.length > 1) {
const err = new Error('You should only render 1 child')
- if (Error.captureStackTrace) Error.captureStackTrace(err, render)
+ if (Error.captureStackTrace) Error.captureStackTrace(err, _render)
throw err
}
@@ -24,20 +106,33 @@ export function render(
const err = new Error(
`You should render an element as a child. Did you forget the as="..." prop?`
)
- if (Error.captureStackTrace) Error.captureStackTrace(err, render)
+ if (Error.captureStackTrace) Error.captureStackTrace(err, _render)
throw err
}
return React.cloneElement(
resolvedChildren,
-
- // Filter out undefined values so that they don't override the existing values
- mergeEventFunctions(compact(passThroughProps), resolvedChildren.props, ['onClick'])
+ Object.assign(
+ {},
+ // Filter out undefined values so that they don't override the existing values
+ mergeEventFunctions(compact(omit(passThroughProps, ['ref'])), resolvedChildren.props, [
+ 'onClick',
+ ]),
+ refRelatedProps
+ )
)
}
}
- return React.createElement(Component, passThroughProps, resolvedChildren)
+ return React.createElement(
+ Component,
+ Object.assign(
+ {},
+ omit(passThroughProps, ['ref']),
+ Component !== React.Fragment && refRelatedProps
+ ),
+ resolvedChildren
+ )
}
/**
@@ -92,3 +187,11 @@ function compact>(object: T) {
}
return clone
}
+
+function omit>(object: T, keysToOmit: string[] = []) {
+ let clone = Object.assign({}, object)
+ for (let key of keysToOmit) {
+ if (key in clone) delete clone[key]
+ }
+ return clone
+}
diff --git a/packages/@headlessui-vue/README.md b/packages/@headlessui-vue/README.md
index 74fef41..5a48a28 100644
--- a/packages/@headlessui-vue/README.md
+++ b/packages/@headlessui-vue/README.md
@@ -338,10 +338,11 @@ To tell an element to render its children directly with no wrapper element, use
##### Props
-| Prop | Type | Default | Description |
-| -------- | ------------------- | ------- | --------------------------------------------------------------------------- |
-| `as` | String \| Component | `div` | The element or component the `MenuItems` should render as. |
-| `static` | Boolean | `false` | Whether the element should ignore the internally managed open/closed state. |
+| Prop | Type | Default | Description |
+| --------- | ------------------- | ------- | --------------------------------------------------------------------------------- |
+| `as` | String \| Component | `div` | The element or component the `MenuItems` 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. |
##### Slot props
@@ -1029,10 +1030,11 @@ export default {
##### Props
-| Prop | Type | Default | Description |
-| -------- | ------------------- | ------- | --------------------------------------------------------------------------- |
-| `as` | String \| Component | `ul` | The element or component the `ListboxOptions` should render as. |
-| `static` | Boolean | `false` | Whether the element should ignore the internally managed open/closed state. |
+| Prop | Type | Default | Description |
+| --------- | ------------------- | ------- | --------------------------------------------------------------------------------- |
+| `as` | String \| Component | `ul` | The element or component the `ListboxOptions` 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. |
##### Slot props
diff --git a/packages/@headlessui-vue/package.json b/packages/@headlessui-vue/package.json
index 15a3639..d635a1b 100644
--- a/packages/@headlessui-vue/package.json
+++ b/packages/@headlessui-vue/package.json
@@ -34,8 +34,8 @@
"@popperjs/core": "^2.5.3",
"@testing-library/vue": "^5.1.0",
"@types/debounce": "^1.2.0",
- "@vue/compiler-sfc": "3.0.0",
- "@vue/test-utils": "^2.0.0-beta.6",
+ "@vue/compiler-sfc": "3.0.1",
+ "@vue/test-utils": "^2.0.0-beta.7",
"husky": "^4.3.0",
"vite": "^1.0.0-rc.4",
"vue": "^3.0.0-rc.13",
diff --git a/packages/@headlessui-vue/src/components/listbox/listbox.test.tsx b/packages/@headlessui-vue/src/components/listbox/listbox.test.tsx
index 441e164..4417cde 100644
--- a/packages/@headlessui-vue/src/components/listbox/listbox.test.tsx
+++ b/packages/@headlessui-vue/src/components/listbox/listbox.test.tsx
@@ -1,4 +1,4 @@
-import { defineComponent, ref, watch } from 'vue'
+import { defineComponent, nextTick, ref, watch } from 'vue'
import { render } from '../../test-utils/vue-testing-library'
import { Listbox, ListboxLabel, ListboxButton, ListboxOptions, ListboxOption } from './listbox'
import { suppressConsoleLogs } from '../../test-utils/suppress-console-logs'
@@ -85,10 +85,10 @@ describe('safeguards', () => {
})
assertListboxButton({
- state: ListboxState.Closed,
+ state: ListboxState.InvisibleUnmounted,
attributes: { id: 'headlessui-listbox-button-1' },
})
- assertListbox({ state: ListboxState.Closed })
+ assertListbox({ state: ListboxState.InvisibleUnmounted })
})
)
})
@@ -119,18 +119,18 @@ describe('Rendering', () => {
})
assertListboxButton({
- state: ListboxState.Closed,
+ state: ListboxState.InvisibleUnmounted,
attributes: { id: 'headlessui-listbox-button-1' },
})
- assertListbox({ state: ListboxState.Closed })
+ assertListbox({ state: ListboxState.InvisibleUnmounted })
await click(getListboxButton())
assertListboxButton({
- state: ListboxState.Open,
+ state: ListboxState.Visible,
attributes: { id: 'headlessui-listbox-button-1' },
})
- assertListbox({ state: ListboxState.Open })
+ assertListbox({ state: ListboxState.Visible })
})
)
})
@@ -155,14 +155,14 @@ describe('Rendering', () => {
})
assertListboxButton({
- state: ListboxState.Closed,
+ state: ListboxState.InvisibleUnmounted,
attributes: { id: 'headlessui-listbox-button-2' },
})
assertListboxLabel({
attributes: { id: 'headlessui-listbox-label-1' },
textContent: JSON.stringify({ open: false }),
})
- assertListbox({ state: ListboxState.Closed })
+ assertListbox({ state: ListboxState.InvisibleUnmounted })
await click(getListboxButton())
@@ -170,7 +170,7 @@ describe('Rendering', () => {
attributes: { id: 'headlessui-listbox-label-1' },
textContent: JSON.stringify({ open: true }),
})
- assertListbox({ state: ListboxState.Open })
+ assertListbox({ state: ListboxState.Visible })
assertListboxLabelLinkedWithListbox()
assertListboxButtonLinkedWithListboxLabel()
})
@@ -199,7 +199,7 @@ describe('Rendering', () => {
textContent: JSON.stringify({ open: false }),
tag: 'p',
})
- assertListbox({ state: ListboxState.Closed })
+ assertListbox({ state: ListboxState.InvisibleUnmounted })
await click(getListboxButton())
assertListboxLabel({
@@ -207,7 +207,7 @@ describe('Rendering', () => {
textContent: JSON.stringify({ open: true }),
tag: 'p',
})
- assertListbox({ state: ListboxState.Open })
+ assertListbox({ state: ListboxState.Visible })
})
)
})
@@ -231,20 +231,20 @@ describe('Rendering', () => {
})
assertListboxButton({
- state: ListboxState.Closed,
+ state: ListboxState.InvisibleUnmounted,
attributes: { id: 'headlessui-listbox-button-1' },
textContent: JSON.stringify({ open: false, focused: false }),
})
- assertListbox({ state: ListboxState.Closed })
+ assertListbox({ state: ListboxState.InvisibleUnmounted })
await click(getListboxButton())
assertListboxButton({
- state: ListboxState.Open,
+ state: ListboxState.Visible,
attributes: { id: 'headlessui-listbox-button-1' },
textContent: JSON.stringify({ open: true, focused: false }),
})
- assertListbox({ state: ListboxState.Open })
+ assertListbox({ state: ListboxState.Visible })
})
)
@@ -266,20 +266,20 @@ describe('Rendering', () => {
})
assertListboxButton({
- state: ListboxState.Closed,
+ state: ListboxState.InvisibleUnmounted,
attributes: { id: 'headlessui-listbox-button-1' },
textContent: JSON.stringify({ open: false, focused: false }),
})
- assertListbox({ state: ListboxState.Closed })
+ assertListbox({ state: ListboxState.InvisibleUnmounted })
await click(getListboxButton())
assertListboxButton({
- state: ListboxState.Open,
+ state: ListboxState.Visible,
attributes: { id: 'headlessui-listbox-button-1' },
textContent: JSON.stringify({ open: true, focused: false }),
})
- assertListbox({ state: ListboxState.Open })
+ assertListbox({ state: ListboxState.Visible })
})
)
@@ -304,10 +304,10 @@ describe('Rendering', () => {
await new Promise(requestAnimationFrame)
assertListboxButton({
- state: ListboxState.Closed,
+ state: ListboxState.InvisibleUnmounted,
attributes: { id: 'headlessui-listbox-button-2' },
})
- assertListbox({ state: ListboxState.Closed })
+ assertListbox({ state: ListboxState.InvisibleUnmounted })
assertListboxButtonLinkedWithListboxLabel()
})
)
@@ -330,19 +330,19 @@ describe('Rendering', () => {
})
assertListboxButton({
- state: ListboxState.Closed,
+ state: ListboxState.InvisibleUnmounted,
attributes: { id: 'headlessui-listbox-button-1' },
})
- assertListbox({ state: ListboxState.Closed })
+ assertListbox({ state: ListboxState.InvisibleUnmounted })
await click(getListboxButton())
assertListboxButton({
- state: ListboxState.Open,
+ state: ListboxState.Visible,
attributes: { id: 'headlessui-listbox-button-1' },
})
assertListbox({
- state: ListboxState.Open,
+ state: ListboxState.Visible,
textContent: JSON.stringify({ open: true }),
})
assertActiveElement(getListbox())
@@ -367,6 +367,31 @@ describe('Rendering', () => {
// Let's verify that the Listbox is already there
expect(getListbox()).not.toBe(null)
})
+
+ it('should be possible to use a different render strategy for the ListboxOptions', async () => {
+ renderTemplate({
+ template: `
+
+ Trigger
+
+ Option A
+ Option B
+ Option C
+
+
+ `,
+ setup: () => ({ value: ref(null) }),
+ })
+
+ await new Promise(nextTick)
+
+ assertListbox({ state: ListboxState.InvisibleHidden })
+
+ // Let's open the Listbox, to see if it is not hidden anymore
+ await click(getListboxButton())
+
+ assertListbox({ state: ListboxState.Visible })
+ })
})
describe('ListboxOption', () => {
@@ -386,19 +411,19 @@ describe('Rendering', () => {
})
assertListboxButton({
- state: ListboxState.Closed,
+ state: ListboxState.InvisibleUnmounted,
attributes: { id: 'headlessui-listbox-button-1' },
})
- assertListbox({ state: ListboxState.Closed })
+ assertListbox({ state: ListboxState.InvisibleUnmounted })
await click(getListboxButton())
assertListboxButton({
- state: ListboxState.Open,
+ state: ListboxState.Visible,
attributes: { id: 'headlessui-listbox-button-1' },
})
assertListbox({
- state: ListboxState.Open,
+ state: ListboxState.Visible,
textContent: JSON.stringify({ active: false, selected: false, disabled: false }),
})
})
@@ -431,10 +456,10 @@ describe('Rendering composition', () => {
})
assertListboxButton({
- state: ListboxState.Closed,
+ state: ListboxState.InvisibleUnmounted,
attributes: { id: 'headlessui-listbox-button-1' },
})
- assertListbox({ state: ListboxState.Closed })
+ assertListbox({ state: ListboxState.InvisibleUnmounted })
// Open Listbox
await click(getListboxButton())
@@ -509,10 +534,10 @@ describe('Rendering composition', () => {
})
assertListboxButton({
- state: ListboxState.Closed,
+ state: ListboxState.InvisibleUnmounted,
attributes: { id: 'headlessui-listbox-button-1' },
})
- assertListbox({ state: ListboxState.Closed })
+ assertListbox({ state: ListboxState.InvisibleUnmounted })
// Open Listbox
await click(getListboxButton())
@@ -543,10 +568,10 @@ describe('Keyboard interactions', () => {
})
assertListboxButton({
- state: ListboxState.Closed,
+ state: ListboxState.InvisibleUnmounted,
attributes: { id: 'headlessui-listbox-button-1' },
})
- assertListbox({ state: ListboxState.Closed })
+ assertListbox({ state: ListboxState.InvisibleUnmounted })
// Focus the button
getListboxButton()?.focus()
@@ -555,9 +580,9 @@ describe('Keyboard interactions', () => {
await press(Keys.Enter)
// Verify it is open
- assertListboxButton({ state: ListboxState.Open })
+ assertListboxButton({ state: ListboxState.Visible })
assertListbox({
- state: ListboxState.Open,
+ state: ListboxState.Visible,
attributes: { id: 'headlessui-listbox-options-2' },
})
assertActiveElement(getListbox())
@@ -592,10 +617,10 @@ describe('Keyboard interactions', () => {
})
assertListboxButton({
- state: ListboxState.Closed,
+ state: ListboxState.InvisibleUnmounted,
attributes: { id: 'headlessui-listbox-button-1' },
})
- assertListbox({ state: ListboxState.Closed })
+ assertListbox({ state: ListboxState.InvisibleUnmounted })
// Focus the button
getListboxButton()?.focus()
@@ -605,10 +630,10 @@ describe('Keyboard interactions', () => {
// Verify it is still closed
assertListboxButton({
- state: ListboxState.Closed,
+ state: ListboxState.InvisibleUnmounted,
attributes: { id: 'headlessui-listbox-button-1' },
})
- assertListbox({ state: ListboxState.Closed })
+ assertListbox({ state: ListboxState.InvisibleUnmounted })
})
)
@@ -630,10 +655,10 @@ describe('Keyboard interactions', () => {
})
assertListboxButton({
- state: ListboxState.Closed,
+ state: ListboxState.InvisibleUnmounted,
attributes: { id: 'headlessui-listbox-button-1' },
})
- assertListbox({ state: ListboxState.Closed })
+ assertListbox({ state: ListboxState.InvisibleUnmounted })
// Focus the button
getListboxButton()?.focus()
@@ -642,9 +667,9 @@ describe('Keyboard interactions', () => {
await press(Keys.Enter)
// Verify it is open
- assertListboxButton({ state: ListboxState.Open })
+ assertListboxButton({ state: ListboxState.Visible })
assertListbox({
- state: ListboxState.Open,
+ state: ListboxState.Visible,
attributes: { id: 'headlessui-listbox-options-2' },
})
assertActiveElement(getListbox())
@@ -660,6 +685,72 @@ describe('Keyboard interactions', () => {
})
)
+ it(
+ 'should be possible to open the listbox with Enter, and focus the selected option (when using the `hidden` render strategy)',
+ suppressConsoleLogs(async () => {
+ renderTemplate({
+ template: `
+
+ Trigger
+
+ Option A
+ Option B
+ Option C
+
+
+ `,
+ setup: () => ({ value: ref('b') }),
+ })
+
+ await new Promise(nextTick)
+
+ assertListboxButton({
+ state: ListboxState.InvisibleHidden,
+ attributes: { id: 'headlessui-listbox-button-1' },
+ })
+ assertListbox({ state: ListboxState.InvisibleHidden })
+
+ // Focus the button
+ getListboxButton()?.focus()
+
+ // Open listbox
+ await press(Keys.Enter)
+
+ // Verify it is visible
+ assertListboxButton({ state: ListboxState.Visible })
+ assertListbox({
+ state: ListboxState.Visible,
+ attributes: { id: 'headlessui-listbox-options-2' },
+ })
+ assertActiveElement(getListbox())
+ assertListboxButtonLinkedWithListbox()
+
+ const options = getListboxOptions()
+
+ // Hover over Option A
+ await mouseMove(options[0])
+
+ // Verify that Option A is active
+ assertActiveListboxOption(options[0])
+
+ // Verify that Option B is still selected
+ assertListboxOption(options[1], { selected: true })
+
+ // Close/Hide the listbox
+ await press(Keys.Escape)
+
+ // Re-open the listbox
+ await click(getListboxButton())
+
+ // Verify we have listbox options
+ expect(options).toHaveLength(3)
+ options.forEach((option, i) => assertListboxOption(option, { selected: i === 1 }))
+
+ // Verify that the second listbox option is active (because it is already selected)
+ assertActiveListboxOption(options[1])
+ })
+ )
+
it(
'should be possible to open the listbox with Enter, and focus the selected option (with a list of objects)',
suppressConsoleLogs(async () => {
@@ -685,10 +776,10 @@ describe('Keyboard interactions', () => {
})
assertListboxButton({
- state: ListboxState.Closed,
+ state: ListboxState.InvisibleUnmounted,
attributes: { id: 'headlessui-listbox-button-1' },
})
- assertListbox({ state: ListboxState.Closed })
+ assertListbox({ state: ListboxState.InvisibleUnmounted })
// Focus the button
getListboxButton()?.focus()
@@ -697,9 +788,9 @@ describe('Keyboard interactions', () => {
await press(Keys.Enter)
// Verify it is open
- assertListboxButton({ state: ListboxState.Open })
+ assertListboxButton({ state: ListboxState.Visible })
assertListbox({
- state: ListboxState.Open,
+ state: ListboxState.Visible,
attributes: { id: 'headlessui-listbox-options-2' },
})
assertActiveElement(getListbox())
@@ -728,14 +819,14 @@ describe('Keyboard interactions', () => {
setup: () => ({ value: ref(null) }),
})
- assertListbox({ state: ListboxState.Closed })
+ assertListbox({ state: ListboxState.InvisibleUnmounted })
// Focus the button
getListboxButton()?.focus()
// Open listbox
await press(Keys.Enter)
- assertListbox({ state: ListboxState.Open })
+ assertListbox({ state: ListboxState.Visible })
assertActiveElement(getListbox())
assertNoActiveListboxOption()
@@ -762,10 +853,10 @@ describe('Keyboard interactions', () => {
})
assertListboxButton({
- state: ListboxState.Closed,
+ state: ListboxState.InvisibleUnmounted,
attributes: { id: 'headlessui-listbox-button-1' },
})
- assertListbox({ state: ListboxState.Closed })
+ assertListbox({ state: ListboxState.InvisibleUnmounted })
// Focus the button
getListboxButton()?.focus()
@@ -802,10 +893,10 @@ describe('Keyboard interactions', () => {
})
assertListboxButton({
- state: ListboxState.Closed,
+ state: ListboxState.InvisibleUnmounted,
attributes: { id: 'headlessui-listbox-button-1' },
})
- assertListbox({ state: ListboxState.Closed })
+ assertListbox({ state: ListboxState.InvisibleUnmounted })
// Focus the button
getListboxButton()?.focus()
@@ -844,10 +935,10 @@ describe('Keyboard interactions', () => {
})
assertListboxButton({
- state: ListboxState.Closed,
+ state: ListboxState.InvisibleUnmounted,
attributes: { id: 'headlessui-listbox-button-1' },
})
- assertListbox({ state: ListboxState.Closed })
+ assertListbox({ state: ListboxState.InvisibleUnmounted })
// Focus the button
getListboxButton()?.focus()
@@ -877,23 +968,23 @@ describe('Keyboard interactions', () => {
})
assertListboxButton({
- state: ListboxState.Closed,
+ state: ListboxState.InvisibleUnmounted,
attributes: { id: 'headlessui-listbox-button-1' },
})
- assertListbox({ state: ListboxState.Closed })
+ assertListbox({ state: ListboxState.InvisibleUnmounted })
// Open listbox
await click(getListboxButton())
// Verify it is open
- assertListboxButton({ state: ListboxState.Open })
+ assertListboxButton({ state: ListboxState.Visible })
// Close listbox
await press(Keys.Enter)
// Verify it is closed
- assertListboxButton({ state: ListboxState.Closed })
- assertListbox({ state: ListboxState.Closed })
+ assertListboxButton({ state: ListboxState.InvisibleUnmounted })
+ assertListbox({ state: ListboxState.InvisibleUnmounted })
// Verify the button is focused again
assertActiveElement(getListboxButton())
@@ -923,16 +1014,16 @@ describe('Keyboard interactions', () => {
})
assertListboxButton({
- state: ListboxState.Closed,
+ state: ListboxState.InvisibleUnmounted,
attributes: { id: 'headlessui-listbox-button-1' },
})
- assertListbox({ state: ListboxState.Closed })
+ assertListbox({ state: ListboxState.InvisibleUnmounted })
// Open listbox
await click(getListboxButton())
// Verify it is open
- assertListboxButton({ state: ListboxState.Open })
+ assertListboxButton({ state: ListboxState.Visible })
// Activate the first listbox option
const options = getListboxOptions()
@@ -942,8 +1033,8 @@ describe('Keyboard interactions', () => {
await press(Keys.Enter)
// Verify it is closed
- assertListboxButton({ state: ListboxState.Closed })
- assertListbox({ state: ListboxState.Closed })
+ assertListboxButton({ state: ListboxState.InvisibleUnmounted })
+ assertListbox({ state: ListboxState.InvisibleUnmounted })
// Verify we got the change event
expect(handleChange).toHaveBeenCalledTimes(1)
@@ -980,10 +1071,10 @@ describe('Keyboard interactions', () => {
})
assertListboxButton({
- state: ListboxState.Closed,
+ state: ListboxState.InvisibleUnmounted,
attributes: { id: 'headlessui-listbox-button-1' },
})
- assertListbox({ state: ListboxState.Closed })
+ assertListbox({ state: ListboxState.InvisibleUnmounted })
// Focus the button
getListboxButton()?.focus()
@@ -992,9 +1083,9 @@ describe('Keyboard interactions', () => {
await press(Keys.Space)
// Verify it is open
- assertListboxButton({ state: ListboxState.Open })
+ assertListboxButton({ state: ListboxState.Visible })
assertListbox({
- state: ListboxState.Open,
+ state: ListboxState.Visible,
attributes: { id: 'headlessui-listbox-options-2' },
})
assertActiveElement(getListbox())
@@ -1026,10 +1117,10 @@ describe('Keyboard interactions', () => {
})
assertListboxButton({
- state: ListboxState.Closed,
+ state: ListboxState.InvisibleUnmounted,
attributes: { id: 'headlessui-listbox-button-1' },
})
- assertListbox({ state: ListboxState.Closed })
+ assertListbox({ state: ListboxState.InvisibleUnmounted })
// Focus the button
getListboxButton()?.focus()
@@ -1039,10 +1130,10 @@ describe('Keyboard interactions', () => {
// Verify it is still closed
assertListboxButton({
- state: ListboxState.Closed,
+ state: ListboxState.InvisibleUnmounted,
attributes: { id: 'headlessui-listbox-button-1' },
})
- assertListbox({ state: ListboxState.Closed })
+ assertListbox({ state: ListboxState.InvisibleUnmounted })
})
)
@@ -1064,10 +1155,10 @@ describe('Keyboard interactions', () => {
})
assertListboxButton({
- state: ListboxState.Closed,
+ state: ListboxState.InvisibleUnmounted,
attributes: { id: 'headlessui-listbox-button-1' },
})
- assertListbox({ state: ListboxState.Closed })
+ assertListbox({ state: ListboxState.InvisibleUnmounted })
// Focus the button
getListboxButton()?.focus()
@@ -1076,9 +1167,9 @@ describe('Keyboard interactions', () => {
await press(Keys.Space)
// Verify it is open
- assertListboxButton({ state: ListboxState.Open })
+ assertListboxButton({ state: ListboxState.Visible })
assertListbox({
- state: ListboxState.Open,
+ state: ListboxState.Visible,
attributes: { id: 'headlessui-listbox-options-2' },
})
assertActiveElement(getListbox())
@@ -1107,14 +1198,14 @@ describe('Keyboard interactions', () => {
setup: () => ({ value: ref(null) }),
})
- assertListbox({ state: ListboxState.Closed })
+ assertListbox({ state: ListboxState.InvisibleUnmounted })
// Focus the button
getListboxButton()?.focus()
// Open listbox
await press(Keys.Space)
- assertListbox({ state: ListboxState.Open })
+ assertListbox({ state: ListboxState.Visible })
assertActiveElement(getListbox())
assertNoActiveListboxOption()
@@ -1141,10 +1232,10 @@ describe('Keyboard interactions', () => {
})
assertListboxButton({
- state: ListboxState.Closed,
+ state: ListboxState.InvisibleUnmounted,
attributes: { id: 'headlessui-listbox-button-1' },
})
- assertListbox({ state: ListboxState.Closed })
+ assertListbox({ state: ListboxState.InvisibleUnmounted })
// Focus the button
getListboxButton()?.focus()
@@ -1181,10 +1272,10 @@ describe('Keyboard interactions', () => {
})
assertListboxButton({
- state: ListboxState.Closed,
+ state: ListboxState.InvisibleUnmounted,
attributes: { id: 'headlessui-listbox-button-1' },
})
- assertListbox({ state: ListboxState.Closed })
+ assertListbox({ state: ListboxState.InvisibleUnmounted })
// Focus the button
getListboxButton()?.focus()
@@ -1223,10 +1314,10 @@ describe('Keyboard interactions', () => {
})
assertListboxButton({
- state: ListboxState.Closed,
+ state: ListboxState.InvisibleUnmounted,
attributes: { id: 'headlessui-listbox-button-1' },
})
- assertListbox({ state: ListboxState.Closed })
+ assertListbox({ state: ListboxState.InvisibleUnmounted })
// Focus the button
getListboxButton()?.focus()
@@ -1261,16 +1352,16 @@ describe('Keyboard interactions', () => {
})
assertListboxButton({
- state: ListboxState.Closed,
+ state: ListboxState.InvisibleUnmounted,
attributes: { id: 'headlessui-listbox-button-1' },
})
- assertListbox({ state: ListboxState.Closed })
+ assertListbox({ state: ListboxState.InvisibleUnmounted })
// Open listbox
await click(getListboxButton())
// Verify it is open
- assertListboxButton({ state: ListboxState.Open })
+ assertListboxButton({ state: ListboxState.Visible })
// Activate the first listbox option
const options = getListboxOptions()
@@ -1280,8 +1371,8 @@ describe('Keyboard interactions', () => {
await press(Keys.Space)
// Verify it is closed
- assertListboxButton({ state: ListboxState.Closed })
- assertListbox({ state: ListboxState.Closed })
+ assertListboxButton({ state: ListboxState.InvisibleUnmounted })
+ assertListbox({ state: ListboxState.InvisibleUnmounted })
// Verify we got the change event
expect(handleChange).toHaveBeenCalledTimes(1)
@@ -1324,9 +1415,9 @@ describe('Keyboard interactions', () => {
await press(Keys.Space)
// Verify it is open
- assertListboxButton({ state: ListboxState.Open })
+ assertListboxButton({ state: ListboxState.Visible })
assertListbox({
- state: ListboxState.Open,
+ state: ListboxState.Visible,
attributes: { id: 'headlessui-listbox-options-2' },
})
assertActiveElement(getListbox())
@@ -1336,8 +1427,8 @@ describe('Keyboard interactions', () => {
await press(Keys.Escape)
// Verify it is closed
- assertListboxButton({ state: ListboxState.Closed })
- assertListbox({ state: ListboxState.Closed })
+ assertListboxButton({ state: ListboxState.InvisibleUnmounted })
+ assertListbox({ state: ListboxState.InvisibleUnmounted })
// Verify the button is focused again
assertActiveElement(getListboxButton())
@@ -1364,10 +1455,10 @@ describe('Keyboard interactions', () => {
})
assertListboxButton({
- state: ListboxState.Closed,
+ state: ListboxState.InvisibleUnmounted,
attributes: { id: 'headlessui-listbox-button-1' },
})
- assertListbox({ state: ListboxState.Closed })
+ assertListbox({ state: ListboxState.InvisibleUnmounted })
// Focus the button
getListboxButton()?.focus()
@@ -1376,9 +1467,9 @@ describe('Keyboard interactions', () => {
await press(Keys.Enter)
// Verify it is open
- assertListboxButton({ state: ListboxState.Open })
+ assertListboxButton({ state: ListboxState.Visible })
assertListbox({
- state: ListboxState.Open,
+ state: ListboxState.Visible,
attributes: { id: 'headlessui-listbox-options-2' },
})
assertActiveElement(getListbox())
@@ -1394,8 +1485,8 @@ describe('Keyboard interactions', () => {
await press(Keys.Tab)
// Verify it is still open
- assertListboxButton({ state: ListboxState.Open })
- assertListbox({ state: ListboxState.Open })
+ assertListboxButton({ state: ListboxState.Visible })
+ assertListbox({ state: ListboxState.Visible })
assertActiveElement(getListbox())
})
)
@@ -1418,10 +1509,10 @@ describe('Keyboard interactions', () => {
})
assertListboxButton({
- state: ListboxState.Closed,
+ state: ListboxState.InvisibleUnmounted,
attributes: { id: 'headlessui-listbox-button-1' },
})
- assertListbox({ state: ListboxState.Closed })
+ assertListbox({ state: ListboxState.InvisibleUnmounted })
// Focus the button
getListboxButton()?.focus()
@@ -1430,9 +1521,9 @@ describe('Keyboard interactions', () => {
await press(Keys.Enter)
// Verify it is open
- assertListboxButton({ state: ListboxState.Open })
+ assertListboxButton({ state: ListboxState.Visible })
assertListbox({
- state: ListboxState.Open,
+ state: ListboxState.Visible,
attributes: { id: 'headlessui-listbox-options-2' },
})
assertActiveElement(getListbox())
@@ -1448,8 +1539,8 @@ describe('Keyboard interactions', () => {
await press(shift(Keys.Tab))
// Verify it is still open
- assertListboxButton({ state: ListboxState.Open })
- assertListbox({ state: ListboxState.Open })
+ assertListboxButton({ state: ListboxState.Visible })
+ assertListbox({ state: ListboxState.Visible })
assertActiveElement(getListbox())
})
)
@@ -1474,10 +1565,10 @@ describe('Keyboard interactions', () => {
})
assertListboxButton({
- state: ListboxState.Closed,
+ state: ListboxState.InvisibleUnmounted,
attributes: { id: 'headlessui-listbox-button-1' },
})
- assertListbox({ state: ListboxState.Closed })
+ assertListbox({ state: ListboxState.InvisibleUnmounted })
// Focus the button
getListboxButton()?.focus()
@@ -1486,9 +1577,9 @@ describe('Keyboard interactions', () => {
await press(Keys.ArrowDown)
// Verify it is open
- assertListboxButton({ state: ListboxState.Open })
+ assertListboxButton({ state: ListboxState.Visible })
assertListbox({
- state: ListboxState.Open,
+ state: ListboxState.Visible,
attributes: { id: 'headlessui-listbox-options-2' },
})
assertActiveElement(getListbox())
@@ -1522,10 +1613,10 @@ describe('Keyboard interactions', () => {
})
assertListboxButton({
- state: ListboxState.Closed,
+ state: ListboxState.InvisibleUnmounted,
attributes: { id: 'headlessui-listbox-button-1' },
})
- assertListbox({ state: ListboxState.Closed })
+ assertListbox({ state: ListboxState.InvisibleUnmounted })
// Focus the button
getListboxButton()?.focus()
@@ -1535,10 +1626,10 @@ describe('Keyboard interactions', () => {
// Verify it is still closed
assertListboxButton({
- state: ListboxState.Closed,
+ state: ListboxState.InvisibleUnmounted,
attributes: { id: 'headlessui-listbox-button-1' },
})
- assertListbox({ state: ListboxState.Closed })
+ assertListbox({ state: ListboxState.InvisibleUnmounted })
})
)
@@ -1560,10 +1651,10 @@ describe('Keyboard interactions', () => {
})
assertListboxButton({
- state: ListboxState.Closed,
+ state: ListboxState.InvisibleUnmounted,
attributes: { id: 'headlessui-listbox-button-1' },
})
- assertListbox({ state: ListboxState.Closed })
+ assertListbox({ state: ListboxState.InvisibleUnmounted })
// Focus the button
getListboxButton()?.focus()
@@ -1572,9 +1663,9 @@ describe('Keyboard interactions', () => {
await press(Keys.ArrowDown)
// Verify it is open
- assertListboxButton({ state: ListboxState.Open })
+ assertListboxButton({ state: ListboxState.Visible })
assertListbox({
- state: ListboxState.Open,
+ state: ListboxState.Visible,
attributes: { id: 'headlessui-listbox-options-2' },
})
assertActiveElement(getListbox())
@@ -1603,14 +1694,14 @@ describe('Keyboard interactions', () => {
setup: () => ({ value: ref(null) }),
})
- assertListbox({ state: ListboxState.Closed })
+ assertListbox({ state: ListboxState.InvisibleUnmounted })
// Focus the button
getListboxButton()?.focus()
// Open listbox
await press(Keys.ArrowDown)
- assertListbox({ state: ListboxState.Open })
+ assertListbox({ state: ListboxState.Visible })
assertActiveElement(getListbox())
assertNoActiveListboxOption()
@@ -1635,10 +1726,10 @@ describe('Keyboard interactions', () => {
})
assertListboxButton({
- state: ListboxState.Closed,
+ state: ListboxState.InvisibleUnmounted,
attributes: { id: 'headlessui-listbox-button-1' },
})
- assertListbox({ state: ListboxState.Closed })
+ assertListbox({ state: ListboxState.InvisibleUnmounted })
// Focus the button
getListboxButton()?.focus()
@@ -1686,10 +1777,10 @@ describe('Keyboard interactions', () => {
})
assertListboxButton({
- state: ListboxState.Closed,
+ state: ListboxState.InvisibleUnmounted,
attributes: { id: 'headlessui-listbox-button-1' },
})
- assertListbox({ state: ListboxState.Closed })
+ assertListbox({ state: ListboxState.InvisibleUnmounted })
// Focus the button
getListboxButton()?.focus()
@@ -1731,10 +1822,10 @@ describe('Keyboard interactions', () => {
})
assertListboxButton({
- state: ListboxState.Closed,
+ state: ListboxState.InvisibleUnmounted,
attributes: { id: 'headlessui-listbox-button-1' },
})
- assertListbox({ state: ListboxState.Closed })
+ assertListbox({ state: ListboxState.InvisibleUnmounted })
// Focus the button
getListboxButton()?.focus()
@@ -1770,10 +1861,10 @@ describe('Keyboard interactions', () => {
})
assertListboxButton({
- state: ListboxState.Closed,
+ state: ListboxState.InvisibleUnmounted,
attributes: { id: 'headlessui-listbox-button-1' },
})
- assertListbox({ state: ListboxState.Closed })
+ assertListbox({ state: ListboxState.InvisibleUnmounted })
// Focus the button
getListboxButton()?.focus()
@@ -1782,9 +1873,9 @@ describe('Keyboard interactions', () => {
await press(Keys.ArrowUp)
// Verify it is open
- assertListboxButton({ state: ListboxState.Open })
+ assertListboxButton({ state: ListboxState.Visible })
assertListbox({
- state: ListboxState.Open,
+ state: ListboxState.Visible,
attributes: { id: 'headlessui-listbox-options-2' },
})
assertActiveElement(getListbox())
@@ -1818,10 +1909,10 @@ describe('Keyboard interactions', () => {
})
assertListboxButton({
- state: ListboxState.Closed,
+ state: ListboxState.InvisibleUnmounted,
attributes: { id: 'headlessui-listbox-button-1' },
})
- assertListbox({ state: ListboxState.Closed })
+ assertListbox({ state: ListboxState.InvisibleUnmounted })
// Focus the button
getListboxButton()?.focus()
@@ -1831,10 +1922,10 @@ describe('Keyboard interactions', () => {
// Verify it is still closed
assertListboxButton({
- state: ListboxState.Closed,
+ state: ListboxState.InvisibleUnmounted,
attributes: { id: 'headlessui-listbox-button-1' },
})
- assertListbox({ state: ListboxState.Closed })
+ assertListbox({ state: ListboxState.InvisibleUnmounted })
})
)
@@ -1856,10 +1947,10 @@ describe('Keyboard interactions', () => {
})
assertListboxButton({
- state: ListboxState.Closed,
+ state: ListboxState.InvisibleUnmounted,
attributes: { id: 'headlessui-listbox-button-1' },
})
- assertListbox({ state: ListboxState.Closed })
+ assertListbox({ state: ListboxState.InvisibleUnmounted })
// Focus the button
getListboxButton()?.focus()
@@ -1868,9 +1959,9 @@ describe('Keyboard interactions', () => {
await press(Keys.ArrowUp)
// Verify it is open
- assertListboxButton({ state: ListboxState.Open })
+ assertListboxButton({ state: ListboxState.Visible })
assertListbox({
- state: ListboxState.Open,
+ state: ListboxState.Visible,
attributes: { id: 'headlessui-listbox-options-2' },
})
assertActiveElement(getListbox())
@@ -1899,14 +1990,14 @@ describe('Keyboard interactions', () => {
setup: () => ({ value: ref(null) }),
})
- assertListbox({ state: ListboxState.Closed })
+ assertListbox({ state: ListboxState.InvisibleUnmounted })
// Focus the button
getListboxButton()?.focus()
// Open listbox
await press(Keys.ArrowUp)
- assertListbox({ state: ListboxState.Open })
+ assertListbox({ state: ListboxState.Visible })
assertActiveElement(getListbox())
assertNoActiveListboxOption()
@@ -1935,10 +2026,10 @@ describe('Keyboard interactions', () => {
})
assertListboxButton({
- state: ListboxState.Closed,
+ state: ListboxState.InvisibleUnmounted,
attributes: { id: 'headlessui-listbox-button-1' },
})
- assertListbox({ state: ListboxState.Closed })
+ assertListbox({ state: ListboxState.InvisibleUnmounted })
// Focus the button
getListboxButton()?.focus()
@@ -1976,10 +2067,10 @@ describe('Keyboard interactions', () => {
})
assertListboxButton({
- state: ListboxState.Closed,
+ state: ListboxState.InvisibleUnmounted,
attributes: { id: 'headlessui-listbox-button-1' },
})
- assertListbox({ state: ListboxState.Closed })
+ assertListbox({ state: ListboxState.InvisibleUnmounted })
// Focus the button
getListboxButton()?.focus()
@@ -2021,10 +2112,10 @@ describe('Keyboard interactions', () => {
})
assertListboxButton({
- state: ListboxState.Closed,
+ state: ListboxState.InvisibleUnmounted,
attributes: { id: 'headlessui-listbox-button-1' },
})
- assertListbox({ state: ListboxState.Closed })
+ assertListbox({ state: ListboxState.InvisibleUnmounted })
// Focus the button
getListboxButton()?.focus()
@@ -2033,9 +2124,9 @@ describe('Keyboard interactions', () => {
await press(Keys.ArrowUp)
// Verify it is open
- assertListboxButton({ state: ListboxState.Open })
+ assertListboxButton({ state: ListboxState.Visible })
assertListbox({
- state: ListboxState.Open,
+ state: ListboxState.Visible,
attributes: { id: 'headlessui-listbox-options-2' },
})
assertActiveElement(getListbox())
@@ -2877,18 +2968,18 @@ describe('Mouse interactions', () => {
})
assertListboxButton({
- state: ListboxState.Closed,
+ state: ListboxState.InvisibleUnmounted,
attributes: { id: 'headlessui-listbox-button-1' },
})
- assertListbox({ state: ListboxState.Closed })
+ assertListbox({ state: ListboxState.InvisibleUnmounted })
// Open listbox
await click(getListboxButton())
// Verify it is open
- assertListboxButton({ state: ListboxState.Open })
+ assertListboxButton({ state: ListboxState.Visible })
assertListbox({
- state: ListboxState.Open,
+ state: ListboxState.Visible,
attributes: { id: 'headlessui-listbox-options-2' },
})
assertActiveElement(getListbox())
@@ -2919,20 +3010,20 @@ describe('Mouse interactions', () => {
})
assertListboxButton({
- state: ListboxState.Closed,
+ state: ListboxState.InvisibleUnmounted,
attributes: { id: 'headlessui-listbox-button-1' },
})
- assertListbox({ state: ListboxState.Closed })
+ assertListbox({ state: ListboxState.InvisibleUnmounted })
// Try to open the listbox
await click(getListboxButton())
// Verify it is still closed
assertListboxButton({
- state: ListboxState.Closed,
+ state: ListboxState.InvisibleUnmounted,
attributes: { id: 'headlessui-listbox-button-1' },
})
- assertListbox({ state: ListboxState.Closed })
+ assertListbox({ state: ListboxState.InvisibleUnmounted })
})
)
@@ -2954,18 +3045,18 @@ describe('Mouse interactions', () => {
})
assertListboxButton({
- state: ListboxState.Closed,
+ state: ListboxState.InvisibleUnmounted,
attributes: { id: 'headlessui-listbox-button-1' },
})
- assertListbox({ state: ListboxState.Closed })
+ assertListbox({ state: ListboxState.InvisibleUnmounted })
// Open listbox
await click(getListboxButton())
// Verify it is open
- assertListboxButton({ state: ListboxState.Open })
+ assertListboxButton({ state: ListboxState.Visible })
assertListbox({
- state: ListboxState.Open,
+ state: ListboxState.Visible,
attributes: { id: 'headlessui-listbox-options-2' },
})
assertActiveElement(getListbox())
@@ -3002,14 +3093,14 @@ describe('Mouse interactions', () => {
await click(getListboxButton())
// Verify it is open
- assertListboxButton({ state: ListboxState.Open })
+ assertListboxButton({ state: ListboxState.Visible })
// Click to close
await click(getListboxButton())
// Verify it is closed
- assertListboxButton({ state: ListboxState.Closed })
- assertListbox({ state: ListboxState.Closed })
+ assertListboxButton({ state: ListboxState.InvisibleUnmounted })
+ assertListbox({ state: ListboxState.InvisibleUnmounted })
})
)
@@ -3059,13 +3150,13 @@ describe('Mouse interactions', () => {
})
// Verify that the window is closed
- assertListbox({ state: ListboxState.Closed })
+ assertListbox({ state: ListboxState.InvisibleUnmounted })
// Click something that is not related to the listbox
await click(document.body)
// Should still be closed
- assertListbox({ state: ListboxState.Closed })
+ assertListbox({ state: ListboxState.InvisibleUnmounted })
})
)
@@ -3088,14 +3179,14 @@ describe('Mouse interactions', () => {
// Open listbox
await click(getListboxButton())
- assertListbox({ state: ListboxState.Open })
+ assertListbox({ state: ListboxState.Visible })
assertActiveElement(getListbox())
// Click something that is not related to the listbox
await click(document.body)
// Should be closed now
- assertListbox({ state: ListboxState.Closed })
+ assertListbox({ state: ListboxState.InvisibleUnmounted })
// Verify the button is focused again
assertActiveElement(getListboxButton())
@@ -3168,14 +3259,14 @@ describe('Mouse interactions', () => {
// Open listbox
await click(getListboxButton())
- assertListbox({ state: ListboxState.Open })
+ assertListbox({ state: ListboxState.Visible })
assertActiveElement(getListbox())
// Click the listbox button again
await click(getListboxButton())
// Should be closed now
- assertListbox({ state: ListboxState.Closed })
+ assertListbox({ state: ListboxState.InvisibleUnmounted })
// Verify the button is focused again
assertActiveElement(getListboxButton())
@@ -3440,14 +3531,14 @@ describe('Mouse interactions', () => {
// Open listbox
await click(getListboxButton())
- assertListbox({ state: ListboxState.Open })
+ assertListbox({ state: ListboxState.Visible })
assertActiveElement(getListbox())
const options = getListboxOptions()
// We should be able to click the first option
await click(options[1])
- assertListbox({ state: ListboxState.Closed })
+ assertListbox({ state: ListboxState.InvisibleUnmounted })
expect(handleChange).toHaveBeenCalledTimes(1)
expect(handleChange).toHaveBeenCalledWith('bob')
@@ -3488,14 +3579,14 @@ describe('Mouse interactions', () => {
// Open listbox
await click(getListboxButton())
- assertListbox({ state: ListboxState.Open })
+ assertListbox({ state: ListboxState.Visible })
assertActiveElement(getListbox())
const options = getListboxOptions()
// We should be able to click the first option
await click(options[1])
- assertListbox({ state: ListboxState.Open })
+ assertListbox({ state: ListboxState.Visible })
assertActiveElement(getListbox())
expect(handleChange).toHaveBeenCalledTimes(0)
@@ -3529,7 +3620,7 @@ describe('Mouse interactions', () => {
// Open listbox
await click(getListboxButton())
- assertListbox({ state: ListboxState.Open })
+ assertListbox({ state: ListboxState.Visible })
assertActiveElement(getListbox())
const options = getListboxOptions()
@@ -3564,7 +3655,7 @@ describe('Mouse interactions', () => {
// Open listbox
await click(getListboxButton())
- assertListbox({ state: ListboxState.Open })
+ assertListbox({ state: ListboxState.Visible })
assertActiveElement(getListbox())
const options = getListboxOptions()
diff --git a/packages/@headlessui-vue/src/components/listbox/listbox.ts b/packages/@headlessui-vue/src/components/listbox/listbox.ts
index dcace66..97048f8 100644
--- a/packages/@headlessui-vue/src/components/listbox/listbox.ts
+++ b/packages/@headlessui-vue/src/components/listbox/listbox.ts
@@ -12,9 +12,10 @@ import {
ComputedRef,
watchEffect,
toRaw,
+ watch,
} from 'vue'
import { match } from '../../utils/match'
-import { render } from '../../utils/render'
+import { Features, render } from '../../utils/render'
import { useId } from '../../hooks/use-id'
import { Keys } from '../../keyboard'
@@ -142,7 +143,10 @@ export const Listbox = defineComponent({
options,
searchQuery,
activeOptionIndex,
- closeListbox: () => (listboxState.value = ListboxStates.Closed),
+ closeListbox: () => {
+ listboxState.value = ListboxStates.Closed
+ activeOptionIndex.value = null
+ },
openListbox: () => (listboxState.value = ListboxStates.Open),
goToOption(focus: Focus, id?: string) {
const nextActiveOptionIndex = calculateActiveOptionIndex(focus, id)
@@ -359,15 +363,11 @@ export const ListboxOptions = defineComponent({
props: {
as: { type: [Object, String], default: 'ul' },
static: { type: Boolean, default: false },
+ unmount: { type: Boolean, default: true },
},
render() {
const api = useListboxContext('ListboxOptions')
- // `static` is a reserved keyword, therefore aliasing it...
- const { static: isStatic, ...passThroughProps } = this.$props
-
- if (!isStatic && api.listboxState.value === ListboxStates.Closed) return null
-
const slot = { open: api.listboxState.value === ListboxStates.Open }
const propsWeControl = {
'aria-activedescendant':
@@ -381,12 +381,15 @@ export const ListboxOptions = defineComponent({
tabIndex: 0,
ref: 'el',
}
+ const passThroughProps = this.$props
return render({
props: { ...passThroughProps, ...propsWeControl },
slot,
attrs: this.$attrs,
slots: this.$slots,
+ features: Features.RenderStrategy | Features.Static,
+ visible: slot.open,
})
},
setup() {
@@ -409,11 +412,11 @@ export const ListboxOptions = defineComponent({
// When in type ahead mode, fallthrough
case Keys.Enter:
event.preventDefault()
- api.closeListbox()
if (api.activeOptionIndex.value !== null) {
const { dataRef } = api.options.value[api.activeOptionIndex.value]
api.select(dataRef.value)
}
+ api.closeListbox()
nextTick(() => api.buttonRef.value?.focus())
break
@@ -496,12 +499,20 @@ export const ListboxOption = defineComponent({
onUnmounted(() => api.unregisterOption(id))
onMounted(() => {
- if (!selected.value) return
- api.goToOption(Focus.Specific, id)
- document.getElementById(id)?.focus?.()
+ watch(
+ [api.listboxState, selected],
+ () => {
+ if (api.listboxState.value !== ListboxStates.Open) return
+ if (!selected.value) return
+ api.goToOption(Focus.Specific, id)
+ document.getElementById(id)?.focus?.()
+ },
+ { immediate: true }
+ )
})
watchEffect(() => {
+ if (api.listboxState.value !== ListboxStates.Open) return
if (!active.value) return
nextTick(() => document.getElementById(id)?.scrollIntoView?.({ block: 'nearest' }))
})
diff --git a/packages/@headlessui-vue/src/components/menu/menu.test.tsx b/packages/@headlessui-vue/src/components/menu/menu.test.tsx
index 127e01c..bd7d041 100644
--- a/packages/@headlessui-vue/src/components/menu/menu.test.tsx
+++ b/packages/@headlessui-vue/src/components/menu/menu.test.tsx
@@ -1,4 +1,4 @@
-import { defineComponent, h } from 'vue'
+import { defineComponent, h, nextTick } from 'vue'
import { render } from '../../test-utils/vue-testing-library'
import { Menu, MenuButton, MenuItems, MenuItem } from './menu'
import { suppressConsoleLogs } from '../../test-utils/suppress-console-logs'
@@ -74,10 +74,10 @@ describe('Safe guards', () => {
`)
assertMenuButton({
- state: MenuState.Closed,
+ state: MenuState.InvisibleUnmounted,
attributes: { id: 'headlessui-menu-button-1' },
})
- assertMenu({ state: MenuState.Closed })
+ assertMenu({ state: MenuState.InvisibleUnmounted })
})
})
@@ -96,20 +96,20 @@ describe('Rendering', () => {
`)
assertMenuButton({
- state: MenuState.Closed,
+ state: MenuState.InvisibleUnmounted,
attributes: { id: 'headlessui-menu-button-1' },
textContent: 'Trigger hidden',
})
- assertMenu({ state: MenuState.Closed })
+ assertMenu({ state: MenuState.InvisibleUnmounted })
await click(getMenuButton())
assertMenuButton({
- state: MenuState.Open,
+ state: MenuState.Visible,
attributes: { id: 'headlessui-menu-button-1' },
textContent: 'Trigger visible',
})
- assertMenu({ state: MenuState.Open })
+ assertMenu({ state: MenuState.Visible })
})
it('should be possible to render a Menu using a template `as` prop', async () => {
@@ -127,18 +127,18 @@ describe('Rendering', () => {
`)
assertMenuButton({
- state: MenuState.Closed,
+ state: MenuState.InvisibleUnmounted,
attributes: { id: 'headlessui-menu-button-1' },
})
- assertMenu({ state: MenuState.Closed })
+ assertMenu({ state: MenuState.InvisibleUnmounted })
await click(getMenuButton())
assertMenuButton({
- state: MenuState.Open,
+ state: MenuState.Visible,
attributes: { id: 'headlessui-menu-button-1' },
})
- assertMenu({ state: MenuState.Open })
+ assertMenu({ state: MenuState.Visible })
})
it(
@@ -178,20 +178,20 @@ describe('Rendering', () => {
`)
assertMenuButton({
- state: MenuState.Closed,
+ state: MenuState.InvisibleUnmounted,
attributes: { id: 'headlessui-menu-button-1' },
textContent: 'Trigger hidden',
})
- assertMenu({ state: MenuState.Closed })
+ assertMenu({ state: MenuState.InvisibleUnmounted })
await click(getMenuButton())
assertMenuButton({
- state: MenuState.Open,
+ state: MenuState.Visible,
attributes: { id: 'headlessui-menu-button-1' },
textContent: 'Trigger visible',
})
- assertMenu({ state: MenuState.Open })
+ assertMenu({ state: MenuState.Visible })
})
it('should be possible to render a MenuButton using a template `as` prop', async () => {
@@ -209,18 +209,18 @@ describe('Rendering', () => {
`)
assertMenuButton({
- state: MenuState.Closed,
+ state: MenuState.InvisibleUnmounted,
attributes: { id: 'headlessui-menu-button-1', 'data-open': 'false' },
})
- assertMenu({ state: MenuState.Closed })
+ assertMenu({ state: MenuState.InvisibleUnmounted })
await click(getMenuButton())
assertMenuButton({
- state: MenuState.Open,
+ state: MenuState.Visible,
attributes: { id: 'headlessui-menu-button-1', 'data-open': 'true' },
})
- assertMenu({ state: MenuState.Open })
+ assertMenu({ state: MenuState.Visible })
})
it(
@@ -262,18 +262,18 @@ describe('Rendering', () => {
`)
assertMenuButton({
- state: MenuState.Closed,
+ state: MenuState.InvisibleUnmounted,
attributes: { id: 'headlessui-menu-button-1' },
})
- assertMenu({ state: MenuState.Closed })
+ assertMenu({ state: MenuState.InvisibleUnmounted })
await click(getMenuButton())
assertMenuButton({
- state: MenuState.Open,
+ state: MenuState.Visible,
attributes: { id: 'headlessui-menu-button-1' },
})
- assertMenu({ state: MenuState.Open })
+ assertMenu({ state: MenuState.Visible })
expect(getMenu()?.firstChild?.textContent).toBe('visible')
})
@@ -292,18 +292,18 @@ describe('Rendering', () => {
`)
assertMenuButton({
- state: MenuState.Closed,
+ state: MenuState.InvisibleUnmounted,
attributes: { id: 'headlessui-menu-button-1' },
})
- assertMenu({ state: MenuState.Closed })
+ assertMenu({ state: MenuState.InvisibleUnmounted })
await click(getMenuButton())
assertMenuButton({
- state: MenuState.Open,
+ state: MenuState.Visible,
attributes: { id: 'headlessui-menu-button-1' },
})
- assertMenu({ state: MenuState.Open, attributes: { 'data-open': 'true' } })
+ assertMenu({ state: MenuState.Visible, attributes: { 'data-open': 'true' } })
})
it('should yell when we render MenuItems using a template `as` prop that contains multiple children', async () => {
@@ -356,6 +356,28 @@ describe('Rendering', () => {
// Let's verify that the Menu is already there
expect(getMenu()).not.toBe(null)
})
+
+ it('should be possible to use a different render strategy for the MenuItems', async () => {
+ renderTemplate(`
+
+ Trigger
+
+ Item A
+ Item B
+ Item C
+
+
+ `)
+
+ await new Promise(nextTick)
+
+ assertMenu({ state: MenuState.InvisibleHidden })
+
+ // Let's open the Menu, to see if it is not hidden anymore
+ await click(getMenuButton())
+
+ assertMenu({ state: MenuState.Visible })
+ })
})
describe('MenuItem', () => {
@@ -374,18 +396,18 @@ describe('Rendering', () => {
`)
assertMenuButton({
- state: MenuState.Closed,
+ state: MenuState.InvisibleUnmounted,
attributes: { id: 'headlessui-menu-button-1' },
})
- assertMenu({ state: MenuState.Closed })
+ assertMenu({ state: MenuState.InvisibleUnmounted })
await click(getMenuButton())
assertMenuButton({
- state: MenuState.Open,
+ state: MenuState.Visible,
attributes: { id: 'headlessui-menu-button-1' },
})
- assertMenu({ state: MenuState.Open })
+ assertMenu({ state: MenuState.Visible })
expect(getMenuItems()[0]?.textContent).toBe(
`Item A - ${JSON.stringify({ active: false, disabled: false })}`
)
@@ -410,20 +432,20 @@ describe('Rendering', () => {
`)
assertMenuButton({
- state: MenuState.Closed,
+ state: MenuState.InvisibleUnmounted,
attributes: { id: 'headlessui-menu-button-1' },
})
- assertMenu({ state: MenuState.Closed })
+ assertMenu({ state: MenuState.InvisibleUnmounted })
getMenuButton()?.focus()
await press(Keys.Enter)
assertMenuButton({
- state: MenuState.Open,
+ state: MenuState.Visible,
attributes: { id: 'headlessui-menu-button-1' },
})
- assertMenu({ state: MenuState.Open })
+ assertMenu({ state: MenuState.Visible })
assertMenuItem(getMenuItems()[0], {
tag: 'a',
attributes: { 'data-active': 'true', 'data-disabled': 'false' },
@@ -492,10 +514,10 @@ describe('Rendering composition', () => {
`)
assertMenuButton({
- state: MenuState.Closed,
+ state: MenuState.InvisibleUnmounted,
attributes: { id: 'headlessui-menu-button-1' },
})
- assertMenu({ state: MenuState.Closed })
+ assertMenu({ state: MenuState.InvisibleUnmounted })
// Open menu
await click(getMenuButton())
@@ -557,10 +579,10 @@ describe('Rendering composition', () => {
})
assertMenuButton({
- state: MenuState.Closed,
+ state: MenuState.InvisibleUnmounted,
attributes: { id: 'headlessui-menu-button-1' },
})
- assertMenu({ state: MenuState.Closed })
+ assertMenu({ state: MenuState.InvisibleUnmounted })
// Open menu
await click(getMenuButton())
@@ -589,10 +611,10 @@ describe('Keyboard interactions', () => {
`)
assertMenuButton({
- state: MenuState.Closed,
+ state: MenuState.InvisibleUnmounted,
attributes: { id: 'headlessui-menu-button-1' },
})
- assertMenu({ state: MenuState.Closed })
+ assertMenu({ state: MenuState.InvisibleUnmounted })
// Focus the button
getMenuButton()?.focus()
@@ -601,9 +623,9 @@ describe('Keyboard interactions', () => {
await press(Keys.Enter)
// Verify it is open
- assertMenuButton({ state: MenuState.Open })
+ assertMenuButton({ state: MenuState.Visible })
assertMenu({
- state: MenuState.Open,
+ state: MenuState.Visible,
attributes: { id: 'headlessui-menu-items-2' },
})
assertMenuButtonLinkedWithMenu()
@@ -630,10 +652,10 @@ describe('Keyboard interactions', () => {
`)
assertMenuButton({
- state: MenuState.Closed,
+ state: MenuState.InvisibleUnmounted,
attributes: { id: 'headlessui-menu-button-1' },
})
- assertMenu({ state: MenuState.Closed })
+ assertMenu({ state: MenuState.InvisibleUnmounted })
// Focus the button
getMenuButton()?.focus()
@@ -643,10 +665,10 @@ describe('Keyboard interactions', () => {
// Verify it is still closed
assertMenuButton({
- state: MenuState.Closed,
+ state: MenuState.InvisibleUnmounted,
attributes: { id: 'headlessui-menu-button-1' },
})
- assertMenu({ state: MenuState.Closed })
+ assertMenu({ state: MenuState.InvisibleUnmounted })
})
it('should have no active menu item when there are no menu items at all', async () => {
@@ -657,14 +679,14 @@ describe('Keyboard interactions', () => {
`)
- assertMenu({ state: MenuState.Closed })
+ assertMenu({ state: MenuState.InvisibleUnmounted })
// Focus the button
getMenuButton()?.focus()
// Open menu
await press(Keys.Enter)
- assertMenu({ state: MenuState.Open })
+ assertMenu({ state: MenuState.Visible })
assertNoActiveMenuItem()
})
@@ -682,10 +704,10 @@ describe('Keyboard interactions', () => {
`)
assertMenuButton({
- state: MenuState.Closed,
+ state: MenuState.InvisibleUnmounted,
attributes: { id: 'headlessui-menu-button-1' },
})
- assertMenu({ state: MenuState.Closed })
+ assertMenu({ state: MenuState.InvisibleUnmounted })
// Focus the button
getMenuButton()?.focus()
@@ -712,10 +734,10 @@ describe('Keyboard interactions', () => {
`)
assertMenuButton({
- state: MenuState.Closed,
+ state: MenuState.InvisibleUnmounted,
attributes: { id: 'headlessui-menu-button-1' },
})
- assertMenu({ state: MenuState.Closed })
+ assertMenu({ state: MenuState.InvisibleUnmounted })
// Focus the button
getMenuButton()?.focus()
@@ -742,10 +764,10 @@ describe('Keyboard interactions', () => {
`)
assertMenuButton({
- state: MenuState.Closed,
+ state: MenuState.InvisibleUnmounted,
attributes: { id: 'headlessui-menu-button-1' },
})
- assertMenu({ state: MenuState.Closed })
+ assertMenu({ state: MenuState.InvisibleUnmounted })
// Focus the button
getMenuButton()?.focus()
@@ -769,23 +791,23 @@ describe('Keyboard interactions', () => {
`)
assertMenuButton({
- state: MenuState.Closed,
+ state: MenuState.InvisibleUnmounted,
attributes: { id: 'headlessui-menu-button-1' },
})
- assertMenu({ state: MenuState.Closed })
+ assertMenu({ state: MenuState.InvisibleUnmounted })
// Open menu
await click(getMenuButton())
// Verify it is open
- assertMenuButton({ state: MenuState.Open })
+ assertMenuButton({ state: MenuState.Visible })
// Close menu
await press(Keys.Enter)
// Verify it is closed
- assertMenuButton({ state: MenuState.Closed })
- assertMenu({ state: MenuState.Closed })
+ assertMenuButton({ state: MenuState.InvisibleUnmounted })
+ assertMenu({ state: MenuState.InvisibleUnmounted })
// Verify the button is focused again
assertActiveElement(getMenuButton())
@@ -808,16 +830,16 @@ describe('Keyboard interactions', () => {
})
assertMenuButton({
- state: MenuState.Closed,
+ state: MenuState.InvisibleUnmounted,
attributes: { id: 'headlessui-menu-button-1' },
})
- assertMenu({ state: MenuState.Closed })
+ assertMenu({ state: MenuState.InvisibleUnmounted })
// Open menu
await click(getMenuButton())
// Verify it is open
- assertMenuButton({ state: MenuState.Open })
+ assertMenuButton({ state: MenuState.Visible })
// Activate the first menu item
const items = getMenuItems()
@@ -827,8 +849,8 @@ describe('Keyboard interactions', () => {
await press(Keys.Enter)
// Verify it is closed
- assertMenuButton({ state: MenuState.Closed })
- assertMenu({ state: MenuState.Closed })
+ assertMenuButton({ state: MenuState.InvisibleUnmounted })
+ assertMenu({ state: MenuState.InvisibleUnmounted })
// Verify the button is focused again
assertActiveElement(getMenuButton())
@@ -860,16 +882,16 @@ describe('Keyboard interactions', () => {
})
assertMenuButton({
- state: MenuState.Closed,
+ state: MenuState.InvisibleUnmounted,
attributes: { id: 'headlessui-menu-button-1' },
})
- assertMenu({ state: MenuState.Closed })
+ assertMenu({ state: MenuState.InvisibleUnmounted })
// Open menu
await click(getMenuButton())
// Verify it is open
- assertMenuButton({ state: MenuState.Open })
+ assertMenuButton({ state: MenuState.Visible })
// Activate the second menu item
const items = getMenuItems()
@@ -879,8 +901,8 @@ describe('Keyboard interactions', () => {
await press(Keys.Enter)
// Verify it is closed
- assertMenuButton({ state: MenuState.Closed })
- assertMenu({ state: MenuState.Closed })
+ assertMenuButton({ state: MenuState.InvisibleUnmounted })
+ assertMenu({ state: MenuState.InvisibleUnmounted })
// Verify the button got "clicked"
expect(clickHandler).toHaveBeenCalledTimes(1)
@@ -918,10 +940,10 @@ describe('Keyboard interactions', () => {
`)
assertMenuButton({
- state: MenuState.Closed,
+ state: MenuState.InvisibleUnmounted,
attributes: { id: 'headlessui-menu-button-1' },
})
- assertMenu({ state: MenuState.Closed })
+ assertMenu({ state: MenuState.InvisibleUnmounted })
// Focus the button
getMenuButton()?.focus()
@@ -930,9 +952,9 @@ describe('Keyboard interactions', () => {
await press(Keys.Space)
// Verify it is open
- assertMenuButton({ state: MenuState.Open })
+ assertMenuButton({ state: MenuState.Visible })
assertMenu({
- state: MenuState.Open,
+ state: MenuState.Visible,
attributes: { id: 'headlessui-menu-items-2' },
})
assertMenuButtonLinkedWithMenu()
@@ -957,10 +979,10 @@ describe('Keyboard interactions', () => {
`)
assertMenuButton({
- state: MenuState.Closed,
+ state: MenuState.InvisibleUnmounted,
attributes: { id: 'headlessui-menu-button-1' },
})
- assertMenu({ state: MenuState.Closed })
+ assertMenu({ state: MenuState.InvisibleUnmounted })
// Focus the button
getMenuButton()?.focus()
@@ -970,10 +992,10 @@ describe('Keyboard interactions', () => {
// Verify it is still closed
assertMenuButton({
- state: MenuState.Closed,
+ state: MenuState.InvisibleUnmounted,
attributes: { id: 'headlessui-menu-button-1' },
})
- assertMenu({ state: MenuState.Closed })
+ assertMenu({ state: MenuState.InvisibleUnmounted })
})
it('should have no active menu item when there are no menu items at all', async () => {
@@ -984,14 +1006,14 @@ describe('Keyboard interactions', () => {
`)
- assertMenu({ state: MenuState.Closed })
+ assertMenu({ state: MenuState.InvisibleUnmounted })
// Focus the button
getMenuButton()?.focus()
// Open menu
await press(Keys.Space)
- assertMenu({ state: MenuState.Open })
+ assertMenu({ state: MenuState.Visible })
assertNoActiveMenuItem()
})
@@ -1009,10 +1031,10 @@ describe('Keyboard interactions', () => {
`)
assertMenuButton({
- state: MenuState.Closed,
+ state: MenuState.InvisibleUnmounted,
attributes: { id: 'headlessui-menu-button-1' },
})
- assertMenu({ state: MenuState.Closed })
+ assertMenu({ state: MenuState.InvisibleUnmounted })
// Focus the button
getMenuButton()?.focus()
@@ -1039,10 +1061,10 @@ describe('Keyboard interactions', () => {
`)
assertMenuButton({
- state: MenuState.Closed,
+ state: MenuState.InvisibleUnmounted,
attributes: { id: 'headlessui-menu-button-1' },
})
- assertMenu({ state: MenuState.Closed })
+ assertMenu({ state: MenuState.InvisibleUnmounted })
// Focus the button
getMenuButton()?.focus()
@@ -1069,10 +1091,10 @@ describe('Keyboard interactions', () => {
`)
assertMenuButton({
- state: MenuState.Closed,
+ state: MenuState.InvisibleUnmounted,
attributes: { id: 'headlessui-menu-button-1' },
})
- assertMenu({ state: MenuState.Closed })
+ assertMenu({ state: MenuState.InvisibleUnmounted })
// Focus the button
getMenuButton()?.focus()
@@ -1098,23 +1120,23 @@ describe('Keyboard interactions', () => {
`)
assertMenuButton({
- state: MenuState.Closed,
+ state: MenuState.InvisibleUnmounted,
attributes: { id: 'headlessui-menu-button-1' },
})
- assertMenu({ state: MenuState.Closed })
+ assertMenu({ state: MenuState.InvisibleUnmounted })
// Open menu
await click(getMenuButton())
// Verify it is open
- assertMenuButton({ state: MenuState.Open })
+ assertMenuButton({ state: MenuState.Visible })
// Close menu
await press(Keys.Space)
// Verify it is closed
- assertMenuButton({ state: MenuState.Closed })
- assertMenu({ state: MenuState.Closed })
+ assertMenuButton({ state: MenuState.InvisibleUnmounted })
+ assertMenu({ state: MenuState.InvisibleUnmounted })
// Verify the button is focused again
assertActiveElement(getMenuButton())
@@ -1140,16 +1162,16 @@ describe('Keyboard interactions', () => {
})
assertMenuButton({
- state: MenuState.Closed,
+ state: MenuState.InvisibleUnmounted,
attributes: { id: 'headlessui-menu-button-1' },
})
- assertMenu({ state: MenuState.Closed })
+ assertMenu({ state: MenuState.InvisibleUnmounted })
// Open menu
await click(getMenuButton())
// Verify it is open
- assertMenuButton({ state: MenuState.Open })
+ assertMenuButton({ state: MenuState.Visible })
// Activate the first menu item
const items = getMenuItems()
@@ -1159,8 +1181,8 @@ describe('Keyboard interactions', () => {
await press(Keys.Space)
// Verify it is closed
- assertMenuButton({ state: MenuState.Closed })
- assertMenu({ state: MenuState.Closed })
+ assertMenuButton({ state: MenuState.InvisibleUnmounted })
+ assertMenu({ state: MenuState.InvisibleUnmounted })
// Verify the "click" went through on the `a` tag
expect(clickHandler).toHaveBeenCalled()
@@ -1191,9 +1213,9 @@ describe('Keyboard interactions', () => {
await press(Keys.Space)
// Verify it is open
- assertMenuButton({ state: MenuState.Open })
+ assertMenuButton({ state: MenuState.Visible })
assertMenu({
- state: MenuState.Open,
+ state: MenuState.Visible,
attributes: { id: 'headlessui-menu-items-2' },
})
assertMenuButtonLinkedWithMenu()
@@ -1202,8 +1224,8 @@ describe('Keyboard interactions', () => {
await press(Keys.Escape)
// Verify it is closed
- assertMenuButton({ state: MenuState.Closed })
- assertMenu({ state: MenuState.Closed })
+ assertMenuButton({ state: MenuState.InvisibleUnmounted })
+ assertMenu({ state: MenuState.InvisibleUnmounted })
// Verify the button is focused again
assertActiveElement(getMenuButton())
@@ -1224,10 +1246,10 @@ describe('Keyboard interactions', () => {
`)
assertMenuButton({
- state: MenuState.Closed,
+ state: MenuState.InvisibleUnmounted,
attributes: { id: 'headlessui-menu-button-1' },
})
- assertMenu({ state: MenuState.Closed })
+ assertMenu({ state: MenuState.InvisibleUnmounted })
// Focus the button
getMenuButton()?.focus()
@@ -1236,9 +1258,9 @@ describe('Keyboard interactions', () => {
await press(Keys.Enter)
// Verify it is open
- assertMenuButton({ state: MenuState.Open })
+ assertMenuButton({ state: MenuState.Visible })
assertMenu({
- state: MenuState.Open,
+ state: MenuState.Visible,
attributes: { id: 'headlessui-menu-items-2' },
})
assertMenuButtonLinkedWithMenu()
@@ -1253,8 +1275,8 @@ describe('Keyboard interactions', () => {
await press(Keys.Tab)
// Verify it is still open
- assertMenuButton({ state: MenuState.Open })
- assertMenu({ state: MenuState.Open })
+ assertMenuButton({ state: MenuState.Visible })
+ assertMenu({ state: MenuState.Visible })
})
it('should focus trap when we use Shift+Tab', async () => {
@@ -1270,10 +1292,10 @@ describe('Keyboard interactions', () => {
`)
assertMenuButton({
- state: MenuState.Closed,
+ state: MenuState.InvisibleUnmounted,
attributes: { id: 'headlessui-menu-button-1' },
})
- assertMenu({ state: MenuState.Closed })
+ assertMenu({ state: MenuState.InvisibleUnmounted })
// Focus the button
getMenuButton()?.focus()
@@ -1282,9 +1304,9 @@ describe('Keyboard interactions', () => {
await press(Keys.Enter)
// Verify it is open
- assertMenuButton({ state: MenuState.Open })
+ assertMenuButton({ state: MenuState.Visible })
assertMenu({
- state: MenuState.Open,
+ state: MenuState.Visible,
attributes: { id: 'headlessui-menu-items-2' },
})
assertMenuButtonLinkedWithMenu()
@@ -1299,8 +1321,8 @@ describe('Keyboard interactions', () => {
await press(shift(Keys.Tab))
// Verify it is still open
- assertMenuButton({ state: MenuState.Open })
- assertMenu({ state: MenuState.Open })
+ assertMenuButton({ state: MenuState.Visible })
+ assertMenu({ state: MenuState.Visible })
})
})
@@ -1318,10 +1340,10 @@ describe('Keyboard interactions', () => {
`)
assertMenuButton({
- state: MenuState.Closed,
+ state: MenuState.InvisibleUnmounted,
attributes: { id: 'headlessui-menu-button-1' },
})
- assertMenu({ state: MenuState.Closed })
+ assertMenu({ state: MenuState.InvisibleUnmounted })
// Focus the button
getMenuButton()?.focus()
@@ -1330,9 +1352,9 @@ describe('Keyboard interactions', () => {
await press(Keys.ArrowDown)
// Verify it is open
- assertMenuButton({ state: MenuState.Open })
+ assertMenuButton({ state: MenuState.Visible })
assertMenu({
- state: MenuState.Open,
+ state: MenuState.Visible,
attributes: { id: 'headlessui-menu-items-2' },
})
assertMenuButtonLinkedWithMenu()
@@ -1359,10 +1381,10 @@ describe('Keyboard interactions', () => {
`)
assertMenuButton({
- state: MenuState.Closed,
+ state: MenuState.InvisibleUnmounted,
attributes: { id: 'headlessui-menu-button-1' },
})
- assertMenu({ state: MenuState.Closed })
+ assertMenu({ state: MenuState.InvisibleUnmounted })
// Focus the button
getMenuButton()?.focus()
@@ -1372,10 +1394,10 @@ describe('Keyboard interactions', () => {
// Verify it is still closed
assertMenuButton({
- state: MenuState.Closed,
+ state: MenuState.InvisibleUnmounted,
attributes: { id: 'headlessui-menu-button-1' },
})
- assertMenu({ state: MenuState.Closed })
+ assertMenu({ state: MenuState.InvisibleUnmounted })
})
it('should have no active menu item when there are no menu items at all', async () => {
@@ -1386,14 +1408,14 @@ describe('Keyboard interactions', () => {
`)
- assertMenu({ state: MenuState.Closed })
+ assertMenu({ state: MenuState.InvisibleUnmounted })
// Focus the button
getMenuButton()?.focus()
// Open menu
await press(Keys.ArrowDown)
- assertMenu({ state: MenuState.Open })
+ assertMenu({ state: MenuState.Visible })
assertNoActiveMenuItem()
})
@@ -1411,10 +1433,10 @@ describe('Keyboard interactions', () => {
`)
assertMenuButton({
- state: MenuState.Closed,
+ state: MenuState.InvisibleUnmounted,
attributes: { id: 'headlessui-menu-button-1' },
})
- assertMenu({ state: MenuState.Closed })
+ assertMenu({ state: MenuState.InvisibleUnmounted })
// Focus the button
getMenuButton()?.focus()
@@ -1454,10 +1476,10 @@ describe('Keyboard interactions', () => {
`)
assertMenuButton({
- state: MenuState.Closed,
+ state: MenuState.InvisibleUnmounted,
attributes: { id: 'headlessui-menu-button-1' },
})
- assertMenu({ state: MenuState.Closed })
+ assertMenu({ state: MenuState.InvisibleUnmounted })
// Focus the button
getMenuButton()?.focus()
@@ -1489,10 +1511,10 @@ describe('Keyboard interactions', () => {
`)
assertMenuButton({
- state: MenuState.Closed,
+ state: MenuState.InvisibleUnmounted,
attributes: { id: 'headlessui-menu-button-1' },
})
- assertMenu({ state: MenuState.Closed })
+ assertMenu({ state: MenuState.InvisibleUnmounted })
// Focus the button
getMenuButton()?.focus()
@@ -1522,10 +1544,10 @@ describe('Keyboard interactions', () => {
`)
assertMenuButton({
- state: MenuState.Closed,
+ state: MenuState.InvisibleUnmounted,
attributes: { id: 'headlessui-menu-button-1' },
})
- assertMenu({ state: MenuState.Closed })
+ assertMenu({ state: MenuState.InvisibleUnmounted })
// Focus the button
getMenuButton()?.focus()
@@ -1534,9 +1556,9 @@ describe('Keyboard interactions', () => {
await press(Keys.ArrowUp)
// Verify it is open
- assertMenuButton({ state: MenuState.Open })
+ assertMenuButton({ state: MenuState.Visible })
assertMenu({
- state: MenuState.Open,
+ state: MenuState.Visible,
attributes: { id: 'headlessui-menu-items-2' },
})
assertMenuButtonLinkedWithMenu()
@@ -1558,14 +1580,14 @@ describe('Keyboard interactions', () => {
`)
- assertMenu({ state: MenuState.Closed })
+ assertMenu({ state: MenuState.InvisibleUnmounted })
// Focus the button
getMenuButton()?.focus()
// Open menu
await press(Keys.ArrowUp)
- assertMenu({ state: MenuState.Open })
+ assertMenu({ state: MenuState.Visible })
assertNoActiveMenuItem()
})
@@ -1583,10 +1605,10 @@ describe('Keyboard interactions', () => {
`)
assertMenuButton({
- state: MenuState.Closed,
+ state: MenuState.InvisibleUnmounted,
attributes: { id: 'headlessui-menu-button-1' },
})
- assertMenu({ state: MenuState.Closed })
+ assertMenu({ state: MenuState.InvisibleUnmounted })
// Focus the button
getMenuButton()?.focus()
@@ -1614,10 +1636,10 @@ describe('Keyboard interactions', () => {
`)
assertMenuButton({
- state: MenuState.Closed,
+ state: MenuState.InvisibleUnmounted,
attributes: { id: 'headlessui-menu-button-1' },
})
- assertMenu({ state: MenuState.Closed })
+ assertMenu({ state: MenuState.InvisibleUnmounted })
// Focus the button
getMenuButton()?.focus()
@@ -1653,10 +1675,10 @@ describe('Keyboard interactions', () => {
`)
assertMenuButton({
- state: MenuState.Closed,
+ state: MenuState.InvisibleUnmounted,
attributes: { id: 'headlessui-menu-button-1' },
})
- assertMenu({ state: MenuState.Closed })
+ assertMenu({ state: MenuState.InvisibleUnmounted })
// Focus the button
getMenuButton()?.focus()
@@ -1665,9 +1687,9 @@ describe('Keyboard interactions', () => {
await press(Keys.ArrowUp)
// Verify it is open
- assertMenuButton({ state: MenuState.Open })
+ assertMenuButton({ state: MenuState.Visible })
assertMenu({
- state: MenuState.Open,
+ state: MenuState.Visible,
attributes: { id: 'headlessui-menu-items-2' },
})
assertMenuButtonLinkedWithMenu()
@@ -2282,18 +2304,18 @@ describe('Mouse interactions', () => {
`)
assertMenuButton({
- state: MenuState.Closed,
+ state: MenuState.InvisibleUnmounted,
attributes: { id: 'headlessui-menu-button-1' },
})
- assertMenu({ state: MenuState.Closed })
+ assertMenu({ state: MenuState.InvisibleUnmounted })
// Open menu
await click(getMenuButton())
// Verify it is open
- assertMenuButton({ state: MenuState.Open })
+ assertMenuButton({ state: MenuState.Visible })
assertMenu({
- state: MenuState.Open,
+ state: MenuState.Visible,
attributes: { id: 'headlessui-menu-items-2' },
})
assertMenuButtonLinkedWithMenu()
@@ -2317,20 +2339,20 @@ describe('Mouse interactions', () => {
`)
assertMenuButton({
- state: MenuState.Closed,
+ state: MenuState.InvisibleUnmounted,
attributes: { id: 'headlessui-menu-button-1' },
})
- assertMenu({ state: MenuState.Closed })
+ assertMenu({ state: MenuState.InvisibleUnmounted })
// Try to open the menu
await click(getMenuButton())
// Verify it is still closed
assertMenuButton({
- state: MenuState.Closed,
+ state: MenuState.InvisibleUnmounted,
attributes: { id: 'headlessui-menu-button-1' },
})
- assertMenu({ state: MenuState.Closed })
+ assertMenu({ state: MenuState.InvisibleUnmounted })
})
it('should be possible to close a menu on click', async () => {
@@ -2349,14 +2371,14 @@ describe('Mouse interactions', () => {
await click(getMenuButton())
// Verify it is open
- assertMenuButton({ state: MenuState.Open })
+ assertMenuButton({ state: MenuState.Visible })
// Click to close
await click(getMenuButton())
// Verify it is closed
- assertMenuButton({ state: MenuState.Closed })
- assertMenu({ state: MenuState.Closed })
+ assertMenuButton({ state: MenuState.InvisibleUnmounted })
+ assertMenu({ state: MenuState.InvisibleUnmounted })
})
it('should focus the menu when you try to focus the button again (when the menu is already open)', async () => {
@@ -2397,13 +2419,13 @@ describe('Mouse interactions', () => {
`)
// Verify that the window is closed
- assertMenu({ state: MenuState.Closed })
+ assertMenu({ state: MenuState.InvisibleUnmounted })
// Click something that is not related to the menu
await click(document.body)
// Should still be closed
- assertMenu({ state: MenuState.Closed })
+ assertMenu({ state: MenuState.InvisibleUnmounted })
})
it('should be possible to click outside of the menu which should close the menu', async () => {
@@ -2420,13 +2442,13 @@ describe('Mouse interactions', () => {
// Open menu
await click(getMenuButton())
- assertMenu({ state: MenuState.Open })
+ assertMenu({ state: MenuState.Visible })
// Click something that is not related to the menu
await click(document.body)
// Should be closed now
- assertMenu({ state: MenuState.Closed })
+ assertMenu({ state: MenuState.InvisibleUnmounted })
// Verify the button is focused again
assertActiveElement(getMenuButton())
@@ -2446,13 +2468,13 @@ describe('Mouse interactions', () => {
// Open menu
await click(getMenuButton())
- assertMenu({ state: MenuState.Open })
+ assertMenu({ state: MenuState.Visible })
// Click the menu button again
await click(getMenuButton())
// Should be closed now
- assertMenu({ state: MenuState.Closed })
+ assertMenu({ state: MenuState.InvisibleUnmounted })
// Verify the button is focused again
assertActiveElement(getMenuButton())
@@ -2706,14 +2728,14 @@ describe('Mouse interactions', () => {
// Open menu
await click(getMenuButton())
- assertMenu({ state: MenuState.Open })
+ assertMenu({ state: MenuState.Visible })
const items = getMenuItems()
// We should be able to click the first item
await click(items[1])
- assertMenu({ state: MenuState.Closed })
+ assertMenu({ state: MenuState.InvisibleUnmounted })
expect(clickHandler).toHaveBeenCalled()
})
@@ -2737,11 +2759,11 @@ describe('Mouse interactions', () => {
// Open menu
await click(getMenuButton())
- assertMenu({ state: MenuState.Open })
+ assertMenu({ state: MenuState.Visible })
// We should be able to click the first item
await click(getMenuItems()[1])
- assertMenu({ state: MenuState.Closed })
+ assertMenu({ state: MenuState.InvisibleUnmounted })
// Verify the callback has been called
expect(clickHandler).toHaveBeenCalledTimes(1)
@@ -2751,7 +2773,7 @@ describe('Mouse interactions', () => {
// Click the last item, which should close and invoke the handler
await click(getMenuItems()[2])
- assertMenu({ state: MenuState.Closed })
+ assertMenu({ state: MenuState.InvisibleUnmounted })
// Verify the callback has been called
expect(clickHandler).toHaveBeenCalledTimes(2)
@@ -2771,13 +2793,13 @@ describe('Mouse interactions', () => {
// Open menu
await click(getMenuButton())
- assertMenu({ state: MenuState.Open })
+ assertMenu({ state: MenuState.Visible })
const items = getMenuItems()
// We should be able to click the first item
await click(items[1])
- assertMenu({ state: MenuState.Open })
+ assertMenu({ state: MenuState.Visible })
})
it('should be possible focus a menu item, so that it becomes active', async () => {
@@ -2794,7 +2816,7 @@ describe('Mouse interactions', () => {
// Open menu
await click(getMenuButton())
- assertMenu({ state: MenuState.Open })
+ assertMenu({ state: MenuState.Visible })
const items = getMenuItems()
@@ -2820,7 +2842,7 @@ describe('Mouse interactions', () => {
// Open menu
await click(getMenuButton())
- assertMenu({ state: MenuState.Open })
+ assertMenu({ state: MenuState.Visible })
const items = getMenuItems()
@@ -2852,7 +2874,7 @@ describe('Mouse interactions', () => {
// Open menu
await click(getMenuButton())
- assertMenu({ state: MenuState.Open })
+ assertMenu({ state: MenuState.Visible })
const items = getMenuItems()
diff --git a/packages/@headlessui-vue/src/components/menu/menu.ts b/packages/@headlessui-vue/src/components/menu/menu.ts
index 2f07315..7738a2c 100644
--- a/packages/@headlessui-vue/src/components/menu/menu.ts
+++ b/packages/@headlessui-vue/src/components/menu/menu.ts
@@ -11,7 +11,7 @@ import {
Ref,
} from 'vue'
import { match } from '../../utils/match'
-import { render } from '../../utils/render'
+import { Features, render } from '../../utils/render'
import { useId } from '../../hooks/use-id'
import { Keys } from '../../keyboard'
@@ -121,7 +121,10 @@ export const Menu = defineComponent({
items,
searchQuery,
activeItemIndex,
- closeMenu: () => (menuState.value = MenuStates.Closed),
+ closeMenu: () => {
+ menuState.value = MenuStates.Closed
+ activeItemIndex.value = null
+ },
openMenu: () => (menuState.value = MenuStates.Open),
goToItem(focus: Focus, id?: string) {
const nextActiveItemIndex = calculateActiveItemIndex(focus, id)
@@ -280,15 +283,11 @@ export const MenuItems = defineComponent({
props: {
as: { type: [Object, String], default: 'div' },
static: { type: Boolean, default: false },
+ unmount: { type: Boolean, default: true },
},
render() {
const api = useMenuContext('MenuItems')
- // `static` is a reserved keyword, therefore aliasing it...
- const { static: isStatic, ...passThroughProps } = this.$props
-
- if (!isStatic && api.menuState.value === MenuStates.Closed) return null
-
const slot = { open: api.menuState.value === MenuStates.Open }
const propsWeControl = {
'aria-activedescendant':
@@ -302,12 +301,15 @@ export const MenuItems = defineComponent({
tabIndex: 0,
ref: 'el',
}
+ const passThroughProps = this.$props
return render({
props: { ...passThroughProps, ...propsWeControl },
slot,
attrs: this.$attrs,
slots: this.$slots,
+ features: Features.RenderStrategy | Features.Static,
+ visible: slot.open,
})
},
setup() {
@@ -330,11 +332,11 @@ export const MenuItems = defineComponent({
// When in type ahead mode, fallthrough
case Keys.Enter:
event.preventDefault()
- api.closeMenu()
if (api.activeItemIndex.value !== null) {
const { id } = api.items.value[api.activeItemIndex.value]
document.getElementById(id)?.click()
}
+ api.closeMenu()
nextTick(() => api.buttonRef.value?.focus())
break
diff --git a/packages/@headlessui-vue/src/test-utils/accessibility-assertions.ts b/packages/@headlessui-vue/src/test-utils/accessibility-assertions.ts
index dd45b20..d70f60f 100644
--- a/packages/@headlessui-vue/src/test-utils/accessibility-assertions.ts
+++ b/packages/@headlessui-vue/src/test-utils/accessibility-assertions.ts
@@ -27,8 +27,14 @@ export function getMenuItems(): HTMLElement[] {
// ---
export enum MenuState {
- Open,
- Closed,
+ /** The menu is visible to the user. */
+ Visible,
+
+ /** The menu is **not** visible to the user. It's still in the DOM, but it is hidden. */
+ InvisibleHidden,
+
+ /** The menu is **not** visible to the user. It's not in the DOM, it is unmounted. */
+ InvisibleUnmounted,
}
export function assertMenuButton(
@@ -47,12 +53,17 @@ export function assertMenuButton(
expect(button).toHaveAttribute('aria-haspopup')
switch (options.state) {
- case MenuState.Open:
+ case MenuState.Visible:
expect(button).toHaveAttribute('aria-controls')
expect(button).toHaveAttribute('aria-expanded', 'true')
break
- case MenuState.Closed:
+ case MenuState.InvisibleHidden:
+ expect(button).toHaveAttribute('aria-controls')
+ expect(button).not.toHaveAttribute('aria-expanded')
+ break
+
+ case MenuState.InvisibleUnmounted:
expect(button).not.toHaveAttribute('aria-controls')
expect(button).not.toHaveAttribute('aria-expanded')
break
@@ -124,27 +135,37 @@ export function assertMenu(
) {
try {
switch (options.state) {
- case MenuState.Open:
+ case MenuState.InvisibleHidden:
if (menu === null) return expect(menu).not.toBe(null)
- // Check that some attributes exists, doesn't really matter what the values are at this point in
- // time, we just require them.
- expect(menu).toHaveAttribute('aria-labelledby')
+ assertHidden(menu)
- // Check that we have the correct values for certain attributes
+ expect(menu).toHaveAttribute('aria-labelledby')
expect(menu).toHaveAttribute('role', 'menu')
- if (options.textContent) {
- expect(menu).toHaveTextContent(options.textContent)
- }
+ if (options.textContent) expect(menu).toHaveTextContent(options.textContent)
- // Ensure menu button has the following attributes
for (let attributeName in options.attributes) {
expect(menu).toHaveAttribute(attributeName, options.attributes[attributeName])
}
break
- case MenuState.Closed:
+ case MenuState.Visible:
+ if (menu === null) return expect(menu).not.toBe(null)
+
+ assertVisible(menu)
+
+ expect(menu).toHaveAttribute('aria-labelledby')
+ expect(menu).toHaveAttribute('role', 'menu')
+
+ if (options.textContent) expect(menu).toHaveTextContent(options.textContent)
+
+ for (let attributeName in options.attributes) {
+ expect(menu).toHaveAttribute(attributeName, options.attributes[attributeName])
+ }
+ break
+
+ case MenuState.InvisibleUnmounted:
expect(menu).toBe(null)
break
@@ -217,8 +238,14 @@ export function getListboxOptions(): HTMLElement[] {
// ---
export enum ListboxState {
- Open,
- Closed,
+ /** The listbox is visible to the user. */
+ Visible,
+
+ /** The listbox is **not** visible to the user. It's still in the DOM, but it is hidden. */
+ InvisibleHidden,
+
+ /** The listbox is **not** visible to the user. It's not in the DOM, it is unmounted. */
+ InvisibleUnmounted,
}
export function assertListbox(
@@ -231,27 +258,37 @@ export function assertListbox(
) {
try {
switch (options.state) {
- case ListboxState.Open:
+ case ListboxState.InvisibleHidden:
if (listbox === null) return expect(listbox).not.toBe(null)
- // Check that some attributes exists, doesn't really matter what the values are at this point in
- // time, we just require them.
- expect(listbox).toHaveAttribute('aria-labelledby')
+ assertHidden(listbox)
- // Check that we have the correct values for certain attributes
+ expect(listbox).toHaveAttribute('aria-labelledby')
expect(listbox).toHaveAttribute('role', 'listbox')
- if (options.textContent) {
- expect(listbox).toHaveTextContent(options.textContent)
- }
+ if (options.textContent) expect(listbox).toHaveTextContent(options.textContent)
- // Ensure listbox button has the following attributes
for (let attributeName in options.attributes) {
expect(listbox).toHaveAttribute(attributeName, options.attributes[attributeName])
}
break
- case ListboxState.Closed:
+ case ListboxState.Visible:
+ if (listbox === null) return expect(listbox).not.toBe(null)
+
+ assertVisible(listbox)
+
+ expect(listbox).toHaveAttribute('aria-labelledby')
+ expect(listbox).toHaveAttribute('role', 'listbox')
+
+ if (options.textContent) expect(listbox).toHaveTextContent(options.textContent)
+
+ for (let attributeName in options.attributes) {
+ expect(listbox).toHaveAttribute(attributeName, options.attributes[attributeName])
+ }
+ break
+
+ case ListboxState.InvisibleUnmounted:
expect(listbox).toBe(null)
break
@@ -280,12 +317,17 @@ export function assertListboxButton(
expect(button).toHaveAttribute('aria-haspopup')
switch (options.state) {
- case ListboxState.Open:
+ case ListboxState.Visible:
expect(button).toHaveAttribute('aria-controls')
expect(button).toHaveAttribute('aria-expanded', 'true')
break
- case ListboxState.Closed:
+ case ListboxState.InvisibleHidden:
+ expect(button).toHaveAttribute('aria-controls')
+ expect(button).not.toHaveAttribute('aria-expanded')
+ break
+
+ case ListboxState.InvisibleUnmounted:
expect(button).not.toHaveAttribute('aria-controls')
expect(button).not.toHaveAttribute('aria-expanded')
break
@@ -567,3 +609,29 @@ export function assertActiveElement(element: HTMLElement | null) {
throw err
}
}
+
+// ---
+
+export function assertHidden(element: HTMLElement | null) {
+ try {
+ if (element === null) return expect(element).not.toBe(null)
+
+ expect(element).toHaveAttribute('hidden')
+ expect(element).toHaveStyle({ display: 'none' })
+ } catch (err) {
+ Error.captureStackTrace(err, assertHidden)
+ throw err
+ }
+}
+
+export function assertVisible(element: HTMLElement | null) {
+ try {
+ if (element === null) return expect(element).not.toBe(null)
+
+ expect(element).not.toHaveAttribute('hidden')
+ expect(element).not.toHaveStyle({ display: 'none' })
+ } catch (err) {
+ Error.captureStackTrace(err, assertVisible)
+ throw err
+ }
+}
diff --git a/packages/@headlessui-vue/src/utils/render.ts b/packages/@headlessui-vue/src/utils/render.ts
index c80e225..d521ba0 100644
--- a/packages/@headlessui-vue/src/utils/render.ts
+++ b/packages/@headlessui-vue/src/utils/render.ts
@@ -1,6 +1,73 @@
import { h, cloneVNode, Slots } from 'vue'
+import { match } from './match'
+
+export enum Features {
+ /** No features at all */
+ None = 0,
+
+ /**
+ * When used, this will allow us to use one of the render strategies.
+ *
+ * **The render strategies are:**
+ * - **Unmount** _(Will unmount the component.)_
+ * - **Hidden** _(Will hide the component using the [hidden] attribute.)_
+ */
+ RenderStrategy = 1,
+
+ /**
+ * When used, this will allow the user of our component to be in control. This can be used when
+ * you want to transition based on some state.
+ */
+ Static = 2,
+}
+
+enum RenderStrategy {
+ Unmount,
+ Hidden,
+}
export function render({
+ visible = true,
+ features = Features.None,
+ ...main
+}: {
+ props: Record
+ slot: Record
+ attrs: Record
+ slots: Slots
+} & {
+ features?: Features
+ visible?: boolean
+}) {
+ // Visible always render
+ if (visible) return _render(main)
+
+ if (features & Features.Static) {
+ // When the `static` prop is passed as `true`, then the user is in control, thus we don't care about anything else
+ if (main.props.static) return _render(main)
+ }
+
+ if (features & Features.RenderStrategy) {
+ const strategy = main.props.unmount ?? true ? RenderStrategy.Unmount : RenderStrategy.Hidden
+
+ return match(strategy, {
+ [RenderStrategy.Unmount]() {
+ return null
+ },
+ [RenderStrategy.Hidden]() {
+ return _render({
+ ...main,
+ props: { ...main.props, hidden: true, style: { display: 'none' } },
+ })
+ },
+ })
+ }
+
+ // No features enabled, just render
+ return _render(main)
+}
+
+function _render({
props,
attrs,
slots,
@@ -11,7 +78,7 @@ export function render({
attrs: Record
slots: Slots
}) {
- const { as, ...passThroughProps } = props
+ const { as, ...passThroughProps } = omit(props, ['unmount', 'static'])
const children = slots.default?.(slot)
@@ -30,3 +97,11 @@ export function render({
return h(as, passThroughProps, children)
}
+
+function omit>(object: T, keysToOmit: string[] = []) {
+ let clone = Object.assign({}, object)
+ for (let key of keysToOmit) {
+ if (key in clone) delete clone[key]
+ }
+ return clone
+}
diff --git a/yarn.lock b/yarn.lock
index 3d968c5..cb8078c 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -363,6 +363,11 @@
resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.11.5.tgz#c7ff6303df71080ec7a4f5b8c003c58f1cf51037"
integrity sha512-X9rD8qqm695vgmeaQ4fvz/o3+Wk4ZzQvSHkDBgpYKxpD4qTAUm88ZKtHkVqIOsYFFbIQ6wQYhC6q7pjqVK0E0Q==
+"@babel/parser@^7.12.0":
+ version "7.12.3"
+ resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.12.3.tgz#a305415ebe7a6c7023b40b5122a0662d928334cd"
+ integrity sha512-kFsOS0IbsuhO5ojF8Hc8z/8vEIOkylVBrjiZUbLTE3XFe0Qi+uu6HjzQixkFaqr0ZPAMZcBVxEwmsnsLPZ2Xsw==
+
"@babel/plugin-proposal-async-generator-functions@^7.10.4":
version "7.10.5"
resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.10.5.tgz#3491cabf2f7c179ab820606cec27fed15e0e8558"
@@ -1065,6 +1070,15 @@
lodash "^4.17.13"
to-fast-properties "^2.0.0"
+"@babel/types@^7.12.0":
+ version "7.12.1"
+ resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.12.1.tgz#e109d9ab99a8de735be287ee3d6a9947a190c4ae"
+ integrity sha512-BzSY3NJBKM4kyatSOWh3D/JJ2O3CVzBybHWxtgxnggaxEuaSTTDqeiSb/xk9lrkw2Tbqyivw5ZU4rT+EfznQsA==
+ dependencies:
+ "@babel/helper-validator-identifier" "^7.10.4"
+ lodash "^4.17.19"
+ to-fast-properties "^2.0.0"
+
"@bcoe/v8-coverage@^0.2.3":
version "0.2.3"
resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39"
@@ -1491,10 +1505,10 @@
hex-rgb "^4.1.0"
postcss-selector-parser "^6.0.2"
-"@testing-library/dom@^7.24.2", "@testing-library/dom@^7.5.7":
- version "7.24.2"
- resolved "https://registry.yarnpkg.com/@testing-library/dom/-/dom-7.24.2.tgz#6d2b7dd21efbd5358b98c2777fc47c252f3ae55e"
- integrity sha512-ERxcZSoHx0EcN4HfshySEWmEf5Kkmgi+J7O79yCJ3xggzVlBJ2w/QjJUC+EBkJJ2OeSw48i3IoePN4w8JlVUIA==
+"@testing-library/dom@^7.24.3":
+ version "7.24.3"
+ resolved "https://registry.yarnpkg.com/@testing-library/dom/-/dom-7.24.3.tgz#dae3071463cf28dc7755b43d9cf2202e34cbb85d"
+ integrity sha512-6eW9fUhEbR423FZvoHRwbWm9RUUByLWGayYFNVvqTnQLYvsNpBS4uEuKH9aqr3trhxFwGVneJUonehL3B1sHJw==
dependencies:
"@babel/code-frame" "^7.10.4"
"@babel/runtime" "^7.10.3"
@@ -1504,10 +1518,24 @@
dom-accessibility-api "^0.5.1"
pretty-format "^26.4.2"
-"@testing-library/dom@^7.24.3":
- version "7.24.3"
- resolved "https://registry.yarnpkg.com/@testing-library/dom/-/dom-7.24.3.tgz#dae3071463cf28dc7755b43d9cf2202e34cbb85d"
- integrity sha512-6eW9fUhEbR423FZvoHRwbWm9RUUByLWGayYFNVvqTnQLYvsNpBS4uEuKH9aqr3trhxFwGVneJUonehL3B1sHJw==
+"@testing-library/dom@^7.26.0":
+ version "7.26.0"
+ resolved "https://registry.yarnpkg.com/@testing-library/dom/-/dom-7.26.0.tgz#da4d052dc426a4ccc916303369c6e7552126f680"
+ integrity sha512-fyKFrBbS1IigaE3FV21LyeC7kSGF84lqTlSYdKmGaHuK2eYQ/bXVPM5vAa2wx/AU1iPD6oQHsxy2QQ17q9AMCg==
+ dependencies:
+ "@babel/code-frame" "^7.10.4"
+ "@babel/runtime" "^7.10.3"
+ "@types/aria-query" "^4.2.0"
+ aria-query "^4.2.2"
+ chalk "^4.1.0"
+ dom-accessibility-api "^0.5.1"
+ lz-string "^1.4.4"
+ pretty-format "^26.4.2"
+
+"@testing-library/dom@^7.5.7":
+ version "7.24.2"
+ resolved "https://registry.yarnpkg.com/@testing-library/dom/-/dom-7.24.2.tgz#6d2b7dd21efbd5358b98c2777fc47c252f3ae55e"
+ integrity sha512-ERxcZSoHx0EcN4HfshySEWmEf5Kkmgi+J7O79yCJ3xggzVlBJ2w/QjJUC+EBkJJ2OeSw48i3IoePN4w8JlVUIA==
dependencies:
"@babel/code-frame" "^7.10.4"
"@babel/runtime" "^7.10.3"
@@ -1531,13 +1559,13 @@
lodash "^4.17.15"
redent "^3.0.0"
-"@testing-library/react@^11.0.4":
- version "11.0.4"
- resolved "https://registry.yarnpkg.com/@testing-library/react/-/react-11.0.4.tgz#c84082bfe1593d8fcd475d46baee024452f31dee"
- integrity sha512-U0fZO2zxm7M0CB5h1+lh31lbAwMSmDMEMGpMT3BUPJwIjDEKYWOV4dx7lb3x2Ue0Pyt77gmz/VropuJnSz/Iew==
+"@testing-library/react@^11.1.0":
+ version "11.1.0"
+ resolved "https://registry.yarnpkg.com/@testing-library/react/-/react-11.1.0.tgz#dfb4b3177d05a8ccf156b5fd14a5550e91d7ebe4"
+ integrity sha512-Nfz58jGzW0tgg3irmTB7sa02JLkLnCk+QN3XG6WiaGQYb0Qc4Ok00aujgjdxlIQWZHbb4Zj5ZOIeE9yKFSs4sA==
dependencies:
"@babel/runtime" "^7.11.2"
- "@testing-library/dom" "^7.24.2"
+ "@testing-library/dom" "^7.26.0"
"@testing-library/vue@^5.1.0":
version "5.1.0"
@@ -1779,10 +1807,10 @@
resolved "https://registry.yarnpkg.com/@types/node/-/node-14.11.1.tgz#56af902ad157e763f9ba63d671c39cda3193c835"
integrity sha512-oTQgnd0hblfLsJ6BvJzzSL+Inogp3lq9fGgqRkMB/ziKMgEUaFl801OncOzUmalfzt14N0oPHMK47ipl+wbTIw==
-"@types/node@^14.11.8":
- version "14.11.8"
- resolved "https://registry.yarnpkg.com/@types/node/-/node-14.11.8.tgz#fe2012f2355e4ce08bca44aeb3abbb21cf88d33f"
- integrity sha512-KPcKqKm5UKDkaYPTuXSx8wEP7vE9GnuaXIZKijwRYcePpZFDVuy2a57LarFKiORbHOuTOOwYzxVxcUzsh2P2Pw==
+"@types/node@^14.11.10":
+ version "14.11.10"
+ resolved "https://registry.yarnpkg.com/@types/node/-/node-14.11.10.tgz#8c102aba13bf5253f35146affbf8b26275069bef"
+ integrity sha512-yV1nWZPlMFpoXyoknm4S56y2nlTAuFYaJuQtYRAOU7xA/FJ9RY0Xm7QOkaYMMmr8ESdHIuUb6oQgR/0+2NqlyA==
"@types/normalize-package-data@^2.4.0":
version "2.4.0"
@@ -1834,10 +1862,10 @@
"@types/prop-types" "*"
csstype "^3.0.2"
-"@types/react@^16.9.52":
- version "16.9.52"
- resolved "https://registry.yarnpkg.com/@types/react/-/react-16.9.52.tgz#c46c72d1a1d8d9d666f4dd2066c0e22600ccfde1"
- integrity sha512-EHRjmnxiNivwhGdMh9sz1Yw9AUxTSZFxKqdBWAAzyZx3sufWwx6ogqHYh/WB1m/I4ZpjkoZLExF5QTy2ekVi/Q==
+"@types/react@^16.9.53":
+ version "16.9.53"
+ resolved "https://registry.yarnpkg.com/@types/react/-/react-16.9.53.tgz#40cd4f8b8d6b9528aedd1fff8fcffe7a112a3d23"
+ integrity sha512-4nW60Sd4L7+WMXH1D6jCdVftuW7j4Za6zdp6tJ33Rqv0nk1ZAmQKML9ZLD4H0dehA3FZxXR/GM8gXplf82oNGw==
dependencies:
"@types/prop-types" "*"
csstype "^3.0.2"
@@ -1947,6 +1975,17 @@
estree-walker "^2.0.1"
source-map "^0.6.1"
+"@vue/compiler-core@3.0.1":
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/@vue/compiler-core/-/compiler-core-3.0.1.tgz#3ce57531078c6220be7ea458e41e4bab3522015b"
+ integrity sha512-BbQQj9YVNaNWEPnP4PiFKgW8QSGB3dcPSKCtekx1586m4VA1z8hHNLQnzeygtV8BM4oU6yriiWmOIYghbJHwFw==
+ dependencies:
+ "@babel/parser" "^7.12.0"
+ "@babel/types" "^7.12.0"
+ "@vue/shared" "3.0.1"
+ estree-walker "^2.0.1"
+ source-map "^0.6.1"
+
"@vue/compiler-dom@3.0.0", "@vue/compiler-dom@^3.0.0-rc.5":
version "3.0.0"
resolved "https://registry.yarnpkg.com/@vue/compiler-dom/-/compiler-dom-3.0.0.tgz#4cbb48fcf1f852daef2babcf9953b681ac463526"
@@ -1955,7 +1994,37 @@
"@vue/compiler-core" "3.0.0"
"@vue/shared" "3.0.0"
-"@vue/compiler-sfc@3.0.0", "@vue/compiler-sfc@^3.0.0-rc.5":
+"@vue/compiler-dom@3.0.1":
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/@vue/compiler-dom/-/compiler-dom-3.0.1.tgz#00b12f2e4aa55e624e2a5257e4bed93cf7555f0b"
+ integrity sha512-8cjgswVU2YmV35H9ARZmSlDr1P9VZxUihRwefkrk6Vrsb7kui5C3d/WQ2/su34FSDpyMU1aacUOiL2CV/vdX6w==
+ dependencies:
+ "@vue/compiler-core" "3.0.1"
+ "@vue/shared" "3.0.1"
+
+"@vue/compiler-sfc@3.0.1":
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/@vue/compiler-sfc/-/compiler-sfc-3.0.1.tgz#f340f8f75b5c1c4509e0f3a12c79d1544899b663"
+ integrity sha512-VO5gJ7SyHw0hf1rkKXRlxjXI9+Q4ngcuUWYnyjOSDch7Wtt2IdOEiC82KFWIkfWMpHqA5HPzL2nDmys3y9d19w==
+ dependencies:
+ "@babel/parser" "^7.12.0"
+ "@babel/types" "^7.12.0"
+ "@vue/compiler-core" "3.0.1"
+ "@vue/compiler-dom" "3.0.1"
+ "@vue/compiler-ssr" "3.0.1"
+ "@vue/shared" "3.0.1"
+ consolidate "^0.16.0"
+ estree-walker "^2.0.1"
+ hash-sum "^2.0.0"
+ lru-cache "^5.1.1"
+ magic-string "^0.25.7"
+ merge-source-map "^1.1.0"
+ postcss "^7.0.32"
+ postcss-modules "^3.2.2"
+ postcss-selector-parser "^6.0.4"
+ source-map "^0.6.1"
+
+"@vue/compiler-sfc@^3.0.0-rc.5":
version "3.0.0"
resolved "https://registry.yarnpkg.com/@vue/compiler-sfc/-/compiler-sfc-3.0.0.tgz#efa38037984bd64aae315828aa5c1248c6eadca9"
integrity sha512-1Bn4L5jNRm6tlb79YwqYUGGe+Yc9PRoRSJi67NJX6icdhf84+tRMtESbx1zCLL9QixQXu2+7aLkXHxvh4RpqAA==
@@ -1985,6 +2054,14 @@
"@vue/compiler-dom" "3.0.0"
"@vue/shared" "3.0.0"
+"@vue/compiler-ssr@3.0.1":
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/@vue/compiler-ssr/-/compiler-ssr-3.0.1.tgz#0455b011d72d4ed02faa93610f14981c3d44a079"
+ integrity sha512-U0Vb7BOniw9rY0/YvXNw5EuIuO0dCoZd3XhnDjAKL9A5pSBxTlx6fPJeQ53gV0XH40M5z8q4yXukFqSVTXC6hQ==
+ dependencies:
+ "@vue/compiler-dom" "3.0.1"
+ "@vue/shared" "3.0.1"
+
"@vue/reactivity@3.0.0":
version "3.0.0"
resolved "https://registry.yarnpkg.com/@vue/reactivity/-/reactivity-3.0.0.tgz#fd15632a608650ce2a969c721787e27e2c80aa6b"
@@ -2014,6 +2091,11 @@
resolved "https://registry.yarnpkg.com/@vue/shared/-/shared-3.0.0.tgz#ec089236629ecc0f10346b92f101ff4339169f1a"
integrity sha512-4XWL/avABGxU2E2ZF1eZq3Tj7fvksCMssDZUHOykBIMmh5d+KcAnQMC5XHMhtnA0NAvktYsA2YpdsVwVmhWzvA==
+"@vue/shared@3.0.1":
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/@vue/shared/-/shared-3.0.1.tgz#48196c056726aa7466d0182698524c84f203006b"
+ integrity sha512-/X6AUbTFCyD2BcJnBoacUct8qcv1A5uk1+N+3tbzDVuhGPRmoYrTSnNUuF53C/GIsTkChrEiXaJh2kyo/0tRvw==
+
"@vue/test-utils@^1.0.3", "@vue/test-utils@^1.1.0":
version "1.1.0"
resolved "https://registry.yarnpkg.com/@vue/test-utils/-/test-utils-1.1.0.tgz#76305e73a786c921ede1352849614e26c7113f94"
@@ -2023,10 +2105,10 @@
lodash "^4.17.15"
pretty "^2.0.0"
-"@vue/test-utils@^2.0.0-beta.6":
- version "2.0.0-beta.6"
- resolved "https://registry.yarnpkg.com/@vue/test-utils/-/test-utils-2.0.0-beta.6.tgz#2f7a653b0025cd4236968269c5972e807fa1fb2c"
- integrity sha512-nBj5HHoTD+2xg0OQ93p/Hil5SkFUcNJ5BA2RUnHlOH6a4PVskgMK8dOLyVcZ1ZJif7knjt7yQVJ6K6YwIzeR1A==
+"@vue/test-utils@^2.0.0-beta.7":
+ version "2.0.0-beta.7"
+ resolved "https://registry.yarnpkg.com/@vue/test-utils/-/test-utils-2.0.0-beta.7.tgz#27751991e0b013ee4af487e51e16a58d477e5857"
+ integrity sha512-cAe7VqoxxkxTr/2N93UpW/LQbcUVKC+QRA3ZBq5ZWImtAf/8jtcdC2mQ9g4AKmSvyaKQtqxrRn4i/y5z7yrrKA==
"@webassemblyjs/ast@1.9.0":
version "1.9.0"
@@ -6748,10 +6830,10 @@ lines-and-columns@^1.1.6:
resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.1.6.tgz#1c00c743b433cd0a4e80758f7b64a57440d9ff00"
integrity sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA=
-lint-staged@^10.4.0:
- version "10.4.0"
- resolved "https://registry.yarnpkg.com/lint-staged/-/lint-staged-10.4.0.tgz#d18628f737328e0bbbf87d183f4020930e9a984e"
- integrity sha512-uaiX4U5yERUSiIEQc329vhCTDDwUcSvKdRLsNomkYLRzijk3v8V9GWm2Nz0RMVB87VcuzLvtgy6OsjoH++QHIg==
+lint-staged@^10.4.2:
+ version "10.4.2"
+ resolved "https://registry.yarnpkg.com/lint-staged/-/lint-staged-10.4.2.tgz#9fee4635c4b5ddb845746f237c6d43494ccd21c1"
+ integrity sha512-OLCA9K1hS+Sl179SO6kX0JtnsaKj/MZalEhUj5yAgXsb63qPI/Gfn6Ua1KuZdbfkZNEu3/n5C/obYCu70IMt9g==
dependencies:
chalk "^4.1.0"
cli-truncate "^2.1.0"
@@ -6959,6 +7041,11 @@ lru-cache@^5.1.1:
dependencies:
yallist "^3.0.2"
+lz-string@^1.4.4:
+ version "1.4.4"
+ resolved "https://registry.yarnpkg.com/lz-string/-/lz-string-1.4.4.tgz#c0d8eaf36059f705796e1e344811cf4c498d3a26"
+ integrity sha1-wNjq82BZ9wV5bh40SBHPTEmNOiY=
+
magic-string@^0.25.2, magic-string@^0.25.5, magic-string@^0.25.7:
version "0.25.7"
resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.25.7.tgz#3f497d6fd34c669c6798dcb821f2ef31f5445051"
@@ -8147,6 +8234,16 @@ postcss-selector-parser@^6.0.0, postcss-selector-parser@^6.0.2:
indexes-of "^1.0.1"
uniq "^1.0.1"
+postcss-selector-parser@^6.0.4:
+ version "6.0.4"
+ resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.4.tgz#56075a1380a04604c38b063ea7767a129af5c2b3"
+ integrity sha512-gjMeXBempyInaBqpp8gODmwZ52WaYsVOsfr4L4lDQ7n3ncD6mEyySiDtgzCT+NYC0mmeOLvtsF8iaEf0YT6dBw==
+ dependencies:
+ cssesc "^3.0.0"
+ indexes-of "^1.0.1"
+ uniq "^1.0.1"
+ util-deprecate "^1.0.2"
+
postcss-value-parser@^3.2.3, postcss-value-parser@^3.3.0:
version "3.3.1"
resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz#9ff822547e2893213cf1c30efa51ac5fd1ba8281"
@@ -8410,10 +8507,10 @@ randomfill@^1.0.3:
randombytes "^2.0.5"
safe-buffer "^5.1.0"
-react-dom@^16.13.1:
- version "16.13.1"
- resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.13.1.tgz#c1bd37331a0486c078ee54c4740720993b2e0e7f"
- integrity sha512-81PIMmVLnCNLO/fFOQxdQkvEq/+Hfpv24XNJfpyZhTRfO0QcmQIF/PgCa1zCOj2w1hrn12MFLyaJ/G0+Mxtfag==
+react-dom@^16.14.0:
+ version "16.14.0"
+ resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.14.0.tgz#7ad838ec29a777fb3c75c3a190f661cf92ab8b89"
+ integrity sha512-1gCeQXDLoIqMgqD3IO2Ah9bnf0w9kzhwN5q4FGnHZ67hBm9yePzB5JJAIQCc8x3pFnNlwFq4RidZggNAAkzWWw==
dependencies:
loose-envify "^1.1.0"
object-assign "^4.1.1"
@@ -8430,10 +8527,10 @@ react-refresh@0.8.3:
resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.8.3.tgz#721d4657672d400c5e3c75d063c4a85fb2d5d68f"
integrity sha512-X8jZHc7nCMjaCqoU+V2I0cOhNW+QMBwSUkeXnTi8IPe6zaRWfn60ZzvFDZqWPfmSJfjub7dDW1SP0jaHWLu/hg==
-react@^16.13.1:
- version "16.13.1"
- resolved "https://registry.yarnpkg.com/react/-/react-16.13.1.tgz#2e818822f1a9743122c063d6410d85c1e3afe48e"
- integrity sha512-YMZQQq32xHLX0bz5Mnibv1/LHb3Sqzngu7xstSM+vrkE5Kzr9xE0yMByK5kMoTK30YVJE61WfbxIFFvfeDKT1w==
+react@^16.14.0:
+ version "16.14.0"
+ resolved "https://registry.yarnpkg.com/react/-/react-16.14.0.tgz#94d776ddd0aaa37da3eda8fc5b6b18a4c9a3114d"
+ integrity sha512-0X2CImDkJGApiAlcf0ODKIneSwBPhqJawOa5wCtKbu7ZECrmS26NvtSILynQ66cgkT/RJ4LidJOc3bUESwmU8g==
dependencies:
loose-envify "^1.1.0"
object-assign "^4.1.1"
@@ -9700,10 +9797,10 @@ table@^5.2.3:
slice-ansi "^2.1.0"
string-width "^3.0.0"
-tailwindcss@^1.9.1:
- version "1.9.1"
- resolved "https://registry.yarnpkg.com/tailwindcss/-/tailwindcss-1.9.1.tgz#5cd83b7962c0e22d7608bc502daf4185962995fc"
- integrity sha512-3faxlyPlcWN8AoNEIVQFNsDcrdXS/D9nOGtdknrXvZp4D4E3AGPO2KRPiGG69B2ZUO0V6RvYiW91L2/n9QnBxg==
+tailwindcss@^1.9.4:
+ version "1.9.4"
+ resolved "https://registry.yarnpkg.com/tailwindcss/-/tailwindcss-1.9.4.tgz#5ae8ff84bc8234df22ba5f2c7feafb64bb14da55"
+ integrity sha512-CVeP4J1pDluBM/AF11JPku9Cx+VwQ6MbOcnlobnWVVZnq+xku8sa+XXmYzy/GvE08qD8w+OmpSdN21ZFPoVDRg==
dependencies:
"@fullhuman/postcss-purgecss" "^2.1.2"
autoprefixer "^9.4.5"
@@ -10242,7 +10339,7 @@ use@^3.1.0:
resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f"
integrity sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==
-util-deprecate@^1.0.1, util-deprecate@~1.0.1:
+util-deprecate@^1.0.1, util-deprecate@^1.0.2, util-deprecate@~1.0.1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=