Improve overal codebase, use modern tech like esbuild and TypeScript 4! (#1055)

* use esbuild for React instead of tsdx

* remove tsdx from Vue

* use consistent names

* add jest and prettier

* update scripts

* ignore some folders for prettier

* run lint script instead of tsdx lint

* run prettier en-masse

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

* bump typescript to latest version

* make typescript happy

* cleanup playground package.json

* make esbuild a dev dependency

* make scripts consistent

* fix husky hooks

* add dedicated watch script

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

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

* ignore formatting in the .next folder

* run prettier on playground-react package

* setup playground-vue

Still not 100% working, but getting there!

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

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

* add `clean` script

* move examples folder in playground-vue to root

* ensure new lines for consistency in scripts

* fix typescript issue

* fix typescript issues in playgrounds

* make sure to run prettier on everything it can

* run prettier on all files

* improve error output

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

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

* update changelog
This commit is contained in:
Robin Malfait
2022-01-27 17:07:38 +01:00
committed by GitHub
parent ea26870480
commit fdd2629795
166 changed files with 5137 additions and 5561 deletions
-1
View File
@@ -46,4 +46,3 @@ yarn vue test
``` ```
Please ensure that the tests are passing when submitting a pull request. If you're adding new features to Headless UI, please include tests. Please ensure that the tests are passing when submitting a pull request. If you're adding new features to Headless UI, please include tests.
-1
View File
@@ -12,4 +12,3 @@ contact_links:
- name: Documentation Issue - name: Documentation Issue
url: https://github.com/tailwindlabs/headlessui/issues/new?title=%5BDOCS%5D:%20 url: https://github.com/tailwindlabs/headlessui/issues/new?title=%5BDOCS%5D:%20
about: 'For documentation issues, suggest changes on our documentation repository.' about: 'For documentation issues, suggest changes on our documentation repository.'
+1 -2
View File
@@ -44,7 +44,7 @@ jobs:
id: vars id: vars
run: echo "::set-output name=sha_short::$(git rev-parse --short HEAD)" run: echo "::set-output name=sha_short::$(git rev-parse --short HEAD)"
- name: "Version based on commit: 0.0.0-insiders.${{ steps.vars.outputs.sha_short }}" - name: 'Version based on commit: 0.0.0-insiders.${{ steps.vars.outputs.sha_short }}'
run: npm version -w packages 0.0.0-insiders.${{ steps.vars.outputs.sha_short }} --force --no-git-tag-version run: npm version -w packages 0.0.0-insiders.${{ steps.vars.outputs.sha_short }} --force --no-git-tag-version
- name: Publish - name: Publish
@@ -52,4 +52,3 @@ jobs:
env: env:
CI: true CI: true
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
+4
View File
@@ -0,0 +1,4 @@
dist/
node_modules/
coverage/
.next/
+11
View File
@@ -0,0 +1,11 @@
{
"minify": false,
"jsc": {
"parser": {
"syntax": "typescript",
"tsx": true,
"decorators": false,
"dynamicImport": false
}
}
}
+2
View File
@@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Ensure correct order when conditionally rendering `Menu.Item`, `Listbox.Option` and `RadioGroup.Option` ([#1045](https://github.com/tailwindlabs/headlessui/pull/1045)) - Ensure correct order when conditionally rendering `Menu.Item`, `Listbox.Option` and `RadioGroup.Option` ([#1045](https://github.com/tailwindlabs/headlessui/pull/1045))
- Improve controlled Tabs behaviour ([#1050](https://github.com/tailwindlabs/headlessui/pull/1050)) - Improve controlled Tabs behaviour ([#1050](https://github.com/tailwindlabs/headlessui/pull/1050))
- Improve typeahead search logic ([#1051](https://github.com/tailwindlabs/headlessui/pull/1051)) - Improve typeahead search logic ([#1051](https://github.com/tailwindlabs/headlessui/pull/1051))
- Improve overal codebase, use modern tech like `esbuild` and TypeScript 4! ([#1055](https://github.com/tailwindlabs/headlessui/pull/1055))
### Added ### Added
@@ -23,6 +24,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Ensure correct order when conditionally rendering `MenuItem`, `ListboxOption` and `RadioGroupOption` ([#1045](https://github.com/tailwindlabs/headlessui/pull/1045)) - Ensure correct order when conditionally rendering `MenuItem`, `ListboxOption` and `RadioGroupOption` ([#1045](https://github.com/tailwindlabs/headlessui/pull/1045))
- Improve typeahead search logic ([#1051](https://github.com/tailwindlabs/headlessui/pull/1051)) - Improve typeahead search logic ([#1051](https://github.com/tailwindlabs/headlessui/pull/1051))
- Improve overal codebase, use modern tech like `esbuild` and TypeScript 4! ([#1055](https://github.com/tailwindlabs/headlessui/pull/1055))
### Added ### Added
-1
View File
@@ -49,4 +49,3 @@ For casual chit-chat with others using the library:
## Contributing ## Contributing
If you're interested in contributing to Headless UI, please read our [contributing docs](https://github.com/tailwindlabs/headlessui/blob/main/.github/CONTRIBUTING.md) **before submitting a pull request**. If you're interested in contributing to Headless UI, please read our [contributing docs](https://github.com/tailwindlabs/headlessui/blob/main/.github/CONTRIBUTING.md) **before submitting a pull request**.
+2 -9
View File
@@ -1,17 +1,10 @@
const { createJestConfig: create } = require('tsdx/dist/createJestConfig')
module.exports = function createJestConfig(root, options) { module.exports = function createJestConfig(root, options) {
return Object.assign( return Object.assign(
{},
create(undefined, root),
{ {
rootDir: root, rootDir: root,
setupFilesAfterEnv: ['<rootDir>../../jest/custom-matchers.ts'], setupFilesAfterEnv: ['<rootDir>../../jest/custom-matchers.ts'],
globals: { transform: {
'ts-jest': { '^.+\\.(t|j)sx?$': '@swc/jest',
isolatedModules: true,
tsConfig: '<rootDir>/tsconfig.tsdx.json',
},
}, },
}, },
options options
+18 -6
View File
@@ -14,10 +14,13 @@
"react-playground": "yarn workspace playground-react dev", "react-playground": "yarn workspace playground-react dev",
"playground-react": "yarn workspace playground-react dev", "playground-react": "yarn workspace playground-react dev",
"vue": "yarn workspace @headlessui/vue", "vue": "yarn workspace @headlessui/vue",
"shared": "yarn workspace @headlessui/shared", "playground-vue": "yarn workspace playground-vue dev",
"vue-playground": "yarn workspace playground-vue dev",
"clean": "yarn workspaces run clean",
"build": "yarn workspaces run build", "build": "yarn workspaces run build",
"test": "./scripts/test.sh", "test": "./scripts/test.sh",
"lint": "./scripts/lint.sh" "lint": "./scripts/lint.sh",
"lint-check": "CI=true ./scripts/lint.sh"
}, },
"husky": { "husky": {
"hooks": { "hooks": {
@@ -25,7 +28,7 @@
} }
}, },
"lint-staged": { "lint-staged": {
"*.{js,jsx,ts,tsx}": "tsdx lint" "*": "yarn lint-check"
}, },
"prettier": { "prettier": {
"printWidth": 100, "printWidth": 100,
@@ -34,12 +37,21 @@
"trailingComma": "es5" "trailingComma": "es5"
}, },
"devDependencies": { "devDependencies": {
"@swc/core": "^1.2.131",
"@swc/jest": "^0.2.17",
"@testing-library/jest-dom": "^5.11.9", "@testing-library/jest-dom": "^5.11.9",
"@types/node": "^14.14.22", "@types/node": "^14.14.22",
"esbuild": "^0.14.11",
"husky": "^4.3.8", "husky": "^4.3.8",
"jest": "26",
"lint-staged": "^12.2.1", "lint-staged": "^12.2.1",
"tsdx": "^0.14.1", "npm-run-all": "^4.1.5",
"tslib": "^2.1.0", "prettier": "^2.5.1",
"typescript": "^3.9.7" "rimraf": "^3.0.2",
"tslib": "^2.3.1",
"typescript": "^4.5.4"
},
"dependencies": {
"prettier-plugin-tailwindcss": "^0.1.4"
} }
} }
-1
View File
@@ -36,4 +36,3 @@ For help, discussion about best practices, or any other conversation that would
For casual chit-chat with others using the library: For casual chit-chat with others using the library:
[Join the Tailwind CSS Discord Server](https://discord.gg/7NF8GNe) [Join the Tailwind CSS Discord Server](https://discord.gg/7NF8GNe)
@@ -0,0 +1,7 @@
'use strict'
if (process.env.NODE_ENV === 'production') {
module.exports = require('./headlessui.prod.cjs.js')
} else {
module.exports = require('./headlessui.dev.cjs.js')
}
+17 -5
View File
@@ -4,12 +4,21 @@
"description": "A set of completely unstyled, fully accessible UI components for React, designed to integrate beautifully with Tailwind CSS.", "description": "A set of completely unstyled, fully accessible UI components for React, designed to integrate beautifully with Tailwind CSS.",
"main": "dist/index.js", "main": "dist/index.js",
"typings": "dist/index.d.ts", "typings": "dist/index.d.ts",
"module": "dist/index.esm.js", "module": "dist/headlessui.esm.js",
"license": "MIT", "license": "MIT",
"files": [ "files": [
"README.md", "README.md",
"dist" "dist"
], ],
"exports": {
".": {
"import": {
"default": "./dist/headlessui.esm.js"
},
"require": "./dist/index.js",
"types": "./dist/index.d.ts"
}
},
"sideEffects": false, "sideEffects": false,
"engines": { "engines": {
"node": ">=10" "node": ">=10"
@@ -24,10 +33,12 @@
}, },
"scripts": { "scripts": {
"prepublishOnly": "npm run build", "prepublishOnly": "npm run build",
"build": "../../scripts/build.sh --external:react --external:react-dom",
"watch": "../../scripts/watch.sh --external:react --external:react-dom",
"test": "../../scripts/test.sh", "test": "../../scripts/test.sh",
"build": "../../scripts/build.sh", "lint": "../../scripts/lint.sh",
"watch": "../../scripts/watch.sh", "playground": "yarn workspace playground-react dev",
"lint": "../../scripts/lint.sh" "clean": "rimraf ./dist"
}, },
"peerDependencies": { "peerDependencies": {
"react": "^16 || ^17 || ^18", "react": "^16 || ^17 || ^18",
@@ -39,6 +50,7 @@
"@types/react-dom": "^16.9.10", "@types/react-dom": "^16.9.10",
"react": "^16.14.0", "react": "^16.14.0",
"react-dom": "^16.14.0", "react-dom": "^16.14.0",
"snapshot-diff": "^0.8.1" "snapshot-diff": "^0.8.1",
"esbuild": "^0.11.18"
} }
} }
@@ -481,7 +481,7 @@ describe('Rendering', () => {
<Combobox.Input onChange={NOOP} /> <Combobox.Input onChange={NOOP} />
<Combobox.Button>Trigger</Combobox.Button> <Combobox.Button>Trigger</Combobox.Button>
<Combobox.Options> <Combobox.Options>
{data => ( {(data) => (
<> <>
<Combobox.Option value="a">{JSON.stringify(data)}</Combobox.Option> <Combobox.Option value="a">{JSON.stringify(data)}</Combobox.Option>
</> </>
@@ -639,10 +639,10 @@ describe('Rendering composition', () => {
<Combobox.Input onChange={NOOP} /> <Combobox.Input onChange={NOOP} />
<Combobox.Button>Trigger</Combobox.Button> <Combobox.Button>Trigger</Combobox.Button>
<Combobox.Options> <Combobox.Options>
<Combobox.Option value="a" className={bag => JSON.stringify(bag)}> <Combobox.Option value="a" className={(bag) => JSON.stringify(bag)}>
Option A Option A
</Combobox.Option> </Combobox.Option>
<Combobox.Option value="b" disabled className={bag => JSON.stringify(bag)}> <Combobox.Option value="b" disabled className={(bag) => JSON.stringify(bag)}>
Option B Option B
</Combobox.Option> </Combobox.Option>
<Combobox.Option value="c" className="no-special-treatment"> <Combobox.Option value="c" className="no-special-treatment">
@@ -738,7 +738,7 @@ describe('Rendering composition', () => {
await click(getComboboxButton()) await click(getComboboxButton())
// Verify options are buttons now // Verify options are buttons now
getComboboxOptions().forEach(option => assertComboboxOption(option, { tag: 'button' })) getComboboxOptions().forEach((option) => assertComboboxOption(option, { tag: 'button' }))
}) })
) )
}) })
@@ -767,7 +767,7 @@ describe('Composition', () => {
<Debug name="Transition" fn={orderFn} /> <Debug name="Transition" fn={orderFn} />
<Combobox.Options> <Combobox.Options>
<Combobox.Option value="a"> <Combobox.Option value="a">
{data => ( {(data) => (
<> <>
{JSON.stringify(data)} {JSON.stringify(data)}
<Debug name="Combobox.Option" fn={orderFn} /> <Debug name="Combobox.Option" fn={orderFn} />
@@ -855,7 +855,7 @@ describe('Keyboard interactions', () => {
// Verify we have combobox options // Verify we have combobox options
let options = getComboboxOptions() let options = getComboboxOptions()
expect(options).toHaveLength(3) expect(options).toHaveLength(3)
options.forEach(option => assertComboboxOption(option, { selected: false })) options.forEach((option) => assertComboboxOption(option, { selected: false }))
assertNoActiveComboboxOption() assertNoActiveComboboxOption()
assertNoSelectedComboboxOption() assertNoSelectedComboboxOption()
@@ -1026,7 +1026,7 @@ describe('Keyboard interactions', () => {
<Combobox.Input onChange={NOOP} /> <Combobox.Input onChange={NOOP} />
<Combobox.Button>Trigger</Combobox.Button> <Combobox.Button>Trigger</Combobox.Button>
<Combobox.Options> <Combobox.Options>
{myOptions.map(myOption => ( {myOptions.map((myOption) => (
<Combobox.Option key={myOption.id} value={myOption}> <Combobox.Option key={myOption.id} value={myOption}>
{myOption.name} {myOption.name}
</Combobox.Option> </Combobox.Option>
@@ -1142,7 +1142,7 @@ describe('Keyboard interactions', () => {
// Verify we have combobox options // Verify we have combobox options
let options = getComboboxOptions() let options = getComboboxOptions()
expect(options).toHaveLength(3) expect(options).toHaveLength(3)
options.forEach(option => assertComboboxOption(option)) options.forEach((option) => assertComboboxOption(option))
assertNoActiveComboboxOption() assertNoActiveComboboxOption()
}) })
) )
@@ -1383,7 +1383,7 @@ describe('Keyboard interactions', () => {
// Verify we have combobox options // Verify we have combobox options
let options = getComboboxOptions() let options = getComboboxOptions()
expect(options).toHaveLength(3) expect(options).toHaveLength(3)
options.forEach(option => assertComboboxOption(option)) options.forEach((option) => assertComboboxOption(option))
// Verify that the first combobox option is active // Verify that the first combobox option is active
assertNoActiveComboboxOption() assertNoActiveComboboxOption()
@@ -1539,7 +1539,7 @@ describe('Keyboard interactions', () => {
// Verify we have combobox options // Verify we have combobox options
let options = getComboboxOptions() let options = getComboboxOptions()
expect(options).toHaveLength(3) expect(options).toHaveLength(3)
options.forEach(option => assertComboboxOption(option)) options.forEach((option) => assertComboboxOption(option))
// Verify that the first combobox option is active // Verify that the first combobox option is active
assertNoActiveComboboxOption() assertNoActiveComboboxOption()
@@ -1695,7 +1695,7 @@ describe('Keyboard interactions', () => {
// Verify we have combobox options // Verify we have combobox options
let options = getComboboxOptions() let options = getComboboxOptions()
expect(options).toHaveLength(3) expect(options).toHaveLength(3)
options.forEach(option => assertComboboxOption(option)) options.forEach((option) => assertComboboxOption(option))
// ! ALERT: The LAST option should now be active // ! ALERT: The LAST option should now be active
assertActiveComboboxOption(options[2]) assertActiveComboboxOption(options[2])
@@ -1843,7 +1843,7 @@ describe('Keyboard interactions', () => {
// Verify we have combobox options // Verify we have combobox options
let options = getComboboxOptions() let options = getComboboxOptions()
expect(options).toHaveLength(3) expect(options).toHaveLength(3)
options.forEach(option => assertComboboxOption(option)) options.forEach((option) => assertComboboxOption(option))
assertActiveComboboxOption(options[0]) assertActiveComboboxOption(options[0])
}) })
) )
@@ -1890,7 +1890,7 @@ describe('Keyboard interactions', () => {
// Verify we have combobox options // Verify we have combobox options
let options = getComboboxOptions() let options = getComboboxOptions()
expect(options).toHaveLength(3) expect(options).toHaveLength(3)
options.forEach(option => assertComboboxOption(option)) options.forEach((option) => assertComboboxOption(option))
// ! ALERT: The LAST option should now be active // ! ALERT: The LAST option should now be active
assertActiveComboboxOption(options[2]) assertActiveComboboxOption(options[2])
@@ -2039,7 +2039,7 @@ describe('Keyboard interactions', () => {
// Verify we have combobox options // Verify we have combobox options
let options = getComboboxOptions() let options = getComboboxOptions()
expect(options).toHaveLength(3) expect(options).toHaveLength(3)
options.forEach(option => assertComboboxOption(option)) options.forEach((option) => assertComboboxOption(option))
assertActiveComboboxOption(options[0]) assertActiveComboboxOption(options[0])
}) })
) )
@@ -2059,7 +2059,7 @@ describe('Keyboard interactions', () => {
return ( return (
<Combobox <Combobox
value={value} value={value}
onChange={value => { onChange={(value) => {
setValue(value) setValue(value)
handleChange(value) handleChange(value)
}} }}
@@ -2305,7 +2305,7 @@ describe('Keyboard interactions', () => {
// Verify we have combobox options // Verify we have combobox options
let options = getComboboxOptions() let options = getComboboxOptions()
expect(options).toHaveLength(3) expect(options).toHaveLength(3)
options.forEach(option => assertComboboxOption(option)) options.forEach((option) => assertComboboxOption(option))
// Verify that the first combobox option is active // Verify that the first combobox option is active
assertNoActiveComboboxOption() assertNoActiveComboboxOption()
@@ -2446,7 +2446,7 @@ describe('Keyboard interactions', () => {
// Verify we have combobox options // Verify we have combobox options
let options = getComboboxOptions() let options = getComboboxOptions()
expect(options).toHaveLength(3) expect(options).toHaveLength(3)
options.forEach(option => assertComboboxOption(option)) options.forEach((option) => assertComboboxOption(option))
assertNoActiveComboboxOption() assertNoActiveComboboxOption()
// We should be able to go down once // We should be able to go down once
@@ -2496,7 +2496,7 @@ describe('Keyboard interactions', () => {
// Verify we have combobox options // Verify we have combobox options
let options = getComboboxOptions() let options = getComboboxOptions()
expect(options).toHaveLength(3) expect(options).toHaveLength(3)
options.forEach(option => assertComboboxOption(option)) options.forEach((option) => assertComboboxOption(option))
assertNoActiveComboboxOption() assertNoActiveComboboxOption()
// We should be able to go down once // We should be able to go down once
@@ -2536,7 +2536,7 @@ describe('Keyboard interactions', () => {
// Verify we have combobox options // Verify we have combobox options
let options = getComboboxOptions() let options = getComboboxOptions()
expect(options).toHaveLength(3) expect(options).toHaveLength(3)
options.forEach(option => assertComboboxOption(option)) options.forEach((option) => assertComboboxOption(option))
assertNoActiveComboboxOption() assertNoActiveComboboxOption()
// Open combobox // Open combobox
@@ -2587,7 +2587,7 @@ describe('Keyboard interactions', () => {
// Verify we have combobox options // Verify we have combobox options
let options = getComboboxOptions() let options = getComboboxOptions()
expect(options).toHaveLength(3) expect(options).toHaveLength(3)
options.forEach(option => assertComboboxOption(option)) options.forEach((option) => assertComboboxOption(option))
// Verify that the first combobox option is active // Verify that the first combobox option is active
assertNoActiveComboboxOption() assertNoActiveComboboxOption()
@@ -2729,7 +2729,7 @@ describe('Keyboard interactions', () => {
// Verify we have combobox options // Verify we have combobox options
let options = getComboboxOptions() let options = getComboboxOptions()
expect(options).toHaveLength(3) expect(options).toHaveLength(3)
options.forEach(option => assertComboboxOption(option)) options.forEach((option) => assertComboboxOption(option))
assertNoActiveComboboxOption() assertNoActiveComboboxOption()
// We should be able to go down once // We should be able to go down once
@@ -2779,7 +2779,7 @@ describe('Keyboard interactions', () => {
// Verify we have combobox options // Verify we have combobox options
let options = getComboboxOptions() let options = getComboboxOptions()
expect(options).toHaveLength(3) expect(options).toHaveLength(3)
options.forEach(option => assertComboboxOption(option)) options.forEach((option) => assertComboboxOption(option))
assertNoActiveComboboxOption() assertNoActiveComboboxOption()
// We should be able to go down once // We should be able to go down once
@@ -2819,7 +2819,7 @@ describe('Keyboard interactions', () => {
// Verify we have combobox options // Verify we have combobox options
let options = getComboboxOptions() let options = getComboboxOptions()
expect(options).toHaveLength(3) expect(options).toHaveLength(3)
options.forEach(option => assertComboboxOption(option)) options.forEach((option) => assertComboboxOption(option))
assertNoActiveComboboxOption() assertNoActiveComboboxOption()
// Open combobox // Open combobox
@@ -2869,7 +2869,7 @@ describe('Keyboard interactions', () => {
// Verify we have combobox options // Verify we have combobox options
let options = getComboboxOptions() let options = getComboboxOptions()
expect(options).toHaveLength(3) expect(options).toHaveLength(3)
options.forEach(option => assertComboboxOption(option)) options.forEach((option) => assertComboboxOption(option))
// ! ALERT: The LAST option should now be active // ! ALERT: The LAST option should now be active
assertActiveComboboxOption(options[2]) assertActiveComboboxOption(options[2])
@@ -3017,7 +3017,7 @@ describe('Keyboard interactions', () => {
// Verify we have combobox options // Verify we have combobox options
let options = getComboboxOptions() let options = getComboboxOptions()
expect(options).toHaveLength(3) expect(options).toHaveLength(3)
options.forEach(option => assertComboboxOption(option)) options.forEach((option) => assertComboboxOption(option))
assertActiveComboboxOption(options[0]) assertActiveComboboxOption(options[0])
}) })
) )
@@ -3053,7 +3053,7 @@ describe('Keyboard interactions', () => {
// Verify we have combobox options // Verify we have combobox options
let options = getComboboxOptions() let options = getComboboxOptions()
expect(options).toHaveLength(3) expect(options).toHaveLength(3)
options.forEach(option => assertComboboxOption(option)) options.forEach((option) => assertComboboxOption(option))
assertNoActiveComboboxOption() assertNoActiveComboboxOption()
// Going up or down should select the single available option // Going up or down should select the single available option
@@ -3108,7 +3108,7 @@ describe('Keyboard interactions', () => {
// Verify we have combobox options // Verify we have combobox options
let options = getComboboxOptions() let options = getComboboxOptions()
expect(options).toHaveLength(3) expect(options).toHaveLength(3)
options.forEach(option => assertComboboxOption(option)) options.forEach((option) => assertComboboxOption(option))
assertActiveComboboxOption(options[2]) assertActiveComboboxOption(options[2])
// We should be able to go down once // We should be able to go down once
@@ -3167,7 +3167,7 @@ describe('Keyboard interactions', () => {
// Verify we have combobox options // Verify we have combobox options
let options = getComboboxOptions() let options = getComboboxOptions()
expect(options).toHaveLength(3) expect(options).toHaveLength(3)
options.forEach(option => assertComboboxOption(option)) options.forEach((option) => assertComboboxOption(option))
// ! ALERT: The LAST option should now be active // ! ALERT: The LAST option should now be active
assertActiveComboboxOption(options[2]) assertActiveComboboxOption(options[2])
@@ -3316,7 +3316,7 @@ describe('Keyboard interactions', () => {
// Verify we have combobox options // Verify we have combobox options
let options = getComboboxOptions() let options = getComboboxOptions()
expect(options).toHaveLength(3) expect(options).toHaveLength(3)
options.forEach(option => assertComboboxOption(option)) options.forEach((option) => assertComboboxOption(option))
assertActiveComboboxOption(options[0]) assertActiveComboboxOption(options[0])
}) })
) )
@@ -3894,14 +3894,16 @@ describe('Keyboard interactions', () => {
let filteredPeople = let filteredPeople =
query === '' query === ''
? props.people ? props.people
: props.people.filter(person => person.name.toLowerCase().includes(query.toLowerCase())) : props.people.filter((person) =>
person.name.toLowerCase().includes(query.toLowerCase())
)
return ( return (
<Combobox value={value} onChange={setValue}> <Combobox value={value} onChange={setValue}>
<Combobox.Input onChange={event => setQuery(event.target.value)} /> <Combobox.Input onChange={(event) => setQuery(event.target.value)} />
<Combobox.Button>Trigger</Combobox.Button> <Combobox.Button>Trigger</Combobox.Button>
<Combobox.Options> <Combobox.Options>
{filteredPeople.map(person => ( {filteredPeople.map((person) => (
<Combobox.Option key={person.value} value={person.value} disabled={person.disabled}> <Combobox.Option key={person.value} value={person.value} disabled={person.disabled}>
{person.name} {person.name}
</Combobox.Option> </Combobox.Option>
@@ -4207,7 +4209,7 @@ describe('Mouse interactions', () => {
// Verify we have combobox options // Verify we have combobox options
let options = getComboboxOptions() let options = getComboboxOptions()
expect(options).toHaveLength(3) expect(options).toHaveLength(3)
options.forEach(option => assertComboboxOption(option)) options.forEach((option) => assertComboboxOption(option))
}) })
) )
@@ -4752,7 +4754,7 @@ describe('Mouse interactions', () => {
return ( return (
<Combobox <Combobox
value={value} value={value}
onChange={value => { onChange={(value) => {
setValue(value) setValue(value)
handleChange(value) handleChange(value)
}} }}
@@ -4804,7 +4806,7 @@ describe('Mouse interactions', () => {
return ( return (
<Combobox <Combobox
value={value} value={value}
onChange={value => { onChange={(value) => {
setValue(value) setValue(value)
handleChange(value) handleChange(value)
}} }}
@@ -123,8 +123,8 @@ let reducers: {
let activeOptionIndex = calculateActiveIndex(action, { let activeOptionIndex = calculateActiveIndex(action, {
resolveItems: () => state.options, resolveItems: () => state.options,
resolveActiveIndex: () => state.activeOptionIndex, resolveActiveIndex: () => state.activeOptionIndex,
resolveId: item => item.id, resolveId: (item) => item.id,
resolveDisabled: item => item.dataRef.current.disabled, resolveDisabled: (item) => item.dataRef.current.disabled,
}) })
if (state.activeOptionIndex === activeOptionIndex) return state if (state.activeOptionIndex === activeOptionIndex) return state
@@ -163,7 +163,7 @@ let reducers: {
let currentActiveOption = let currentActiveOption =
state.activeOptionIndex !== null ? nextOptions[state.activeOptionIndex] : null state.activeOptionIndex !== null ? nextOptions[state.activeOptionIndex] : null
let idx = nextOptions.findIndex(a => a.id === action.id) let idx = nextOptions.findIndex((a) => a.id === action.id)
if (idx !== -1) nextOptions.splice(idx, 1) if (idx !== -1) nextOptions.splice(idx, 1)
@@ -284,12 +284,13 @@ export function Combobox<TTag extends ElementType = typeof DEFAULT_COMBOBOX_TAG,
}, [onChange, propsRef]) }, [onChange, propsRef])
useIsoMorphicEffect(() => dispatch({ type: ActionTypes.SetDisabled, disabled }), [disabled]) useIsoMorphicEffect(() => dispatch({ type: ActionTypes.SetDisabled, disabled }), [disabled])
useIsoMorphicEffect(() => dispatch({ type: ActionTypes.SetOrientation, orientation }), [ useIsoMorphicEffect(
orientation, () => dispatch({ type: ActionTypes.SetOrientation, orientation }),
]) [orientation]
)
// Handle outside click // Handle outside click
useWindowEvent('mousedown', event => { useWindowEvent('mousedown', (event) => {
let target = event.target as HTMLElement let target = event.target as HTMLElement
if (comboboxState !== ComboboxStates.Open) return if (comboboxState !== ComboboxStates.Open) return
@@ -333,7 +334,7 @@ export function Combobox<TTag extends ElementType = typeof DEFAULT_COMBOBOX_TAG,
let selectOption = useCallback( let selectOption = useCallback(
(id: string) => { (id: string) => {
let option = options.find(item => item.id === id) let option = options.find((item) => item.id === id)
if (!option) return if (!option) return
let { dataRef } = option let { dataRef } = option
@@ -418,7 +419,7 @@ let Input = forwardRefWithAs(function Input<
ref: Ref<HTMLInputElement> ref: Ref<HTMLInputElement>
) { ) {
let { value, onChange, displayValue, ...passThroughProps } = props let { value, onChange, displayValue, ...passThroughProps } = props
let [state, dispatch] = useComboboxContext([Combobox.name, Input.name].join('.')) let [state, dispatch] = useComboboxContext('Combobox.Input')
let actions = useComboboxActions() let actions = useComboboxActions()
let inputRef = useSyncRefs(state.inputRef, ref) let inputRef = useSyncRefs(state.inputRef, ref)
@@ -579,7 +580,7 @@ let Button = forwardRefWithAs(function Button<TTag extends ElementType = typeof
props: Props<TTag, ButtonRenderPropArg, ButtonPropsWeControl>, props: Props<TTag, ButtonRenderPropArg, ButtonPropsWeControl>,
ref: Ref<HTMLButtonElement> ref: Ref<HTMLButtonElement>
) { ) {
let [state, dispatch] = useComboboxContext([Combobox.name, Button.name].join('.')) let [state, dispatch] = useComboboxContext('Combobox.Button')
let actions = useComboboxActions() let actions = useComboboxActions()
let buttonRef = useSyncRefs(state.buttonRef, ref) let buttonRef = useSyncRefs(state.buttonRef, ref)
@@ -693,12 +694,13 @@ type LabelPropsWeControl = 'id' | 'ref' | 'onClick'
function Label<TTag extends ElementType = typeof DEFAULT_LABEL_TAG>( function Label<TTag extends ElementType = typeof DEFAULT_LABEL_TAG>(
props: Props<TTag, LabelRenderPropArg, LabelPropsWeControl> props: Props<TTag, LabelRenderPropArg, LabelPropsWeControl>
) { ) {
let [state] = useComboboxContext([Combobox.name, Label.name].join('.')) let [state] = useComboboxContext('Combobox.Label')
let id = `headlessui-combobox-label-${useId()}` let id = `headlessui-combobox-label-${useId()}`
let handleClick = useCallback(() => state.inputRef.current?.focus({ preventScroll: true }), [ let handleClick = useCallback(
state.inputRef, () => state.inputRef.current?.focus({ preventScroll: true }),
]) [state.inputRef]
)
let slot = useMemo<LabelRenderPropArg>( let slot = useMemo<LabelRenderPropArg>(
() => ({ open: state.comboboxState === ComboboxStates.Open, disabled: state.disabled }), () => ({ open: state.comboboxState === ComboboxStates.Open, disabled: state.disabled }),
@@ -737,7 +739,7 @@ let Options = forwardRefWithAs(function Options<
PropsForFeatures<typeof OptionsRenderFeatures>, PropsForFeatures<typeof OptionsRenderFeatures>,
ref: Ref<HTMLUListElement> ref: Ref<HTMLUListElement>
) { ) {
let [state, dispatch] = useComboboxContext([Combobox.name, Options.name].join('.')) let [state, dispatch] = useComboboxContext('Combobox.Options')
let optionsRef = useSyncRefs(state.optionsRef, ref) let optionsRef = useSyncRefs(state.optionsRef, ref)
let id = `headlessui-combobox-options-${useId()}` let id = `headlessui-combobox-options-${useId()}`
@@ -751,10 +753,10 @@ let Options = forwardRefWithAs(function Options<
return state.comboboxState === ComboboxStates.Open return state.comboboxState === ComboboxStates.Open
})() })()
let labelledby = useComputed(() => state.labelRef.current?.id ?? state.buttonRef.current?.id, [ let labelledby = useComputed(
state.labelRef.current, () => state.labelRef.current?.id ?? state.buttonRef.current?.id,
state.buttonRef.current, [state.labelRef.current, state.buttonRef.current]
]) )
let handleLeave = useCallback(() => { let handleLeave = useCallback(() => {
if (state.comboboxState !== ComboboxStates.Open) return if (state.comboboxState !== ComboboxStates.Open) return
@@ -820,7 +822,7 @@ function Option<
} }
) { ) {
let { disabled = false, value, ...passthroughProps } = props let { disabled = false, value, ...passthroughProps } = props
let [state, dispatch] = useComboboxContext([Combobox.name, Option.name].join('.')) let [state, dispatch] = useComboboxContext('Combobox.Option')
let actions = useComboboxActions() let actions = useComboboxActions()
let id = `headlessui-combobox-option-${useId()}` let id = `headlessui-combobox-option-${useId()}`
let active = let active =
@@ -886,11 +888,10 @@ function Option<
dispatch({ type: ActionTypes.GoToOption, focus: Focus.Nothing }) dispatch({ type: ActionTypes.GoToOption, focus: Focus.Nothing })
}, [disabled, active, dispatch]) }, [disabled, active, dispatch])
let slot = useMemo<OptionRenderPropArg>(() => ({ active, selected, disabled }), [ let slot = useMemo<OptionRenderPropArg>(
active, () => ({ active, selected, disabled }),
selected, [active, selected, disabled]
disabled, )
])
let propsWeControl = { let propsWeControl = {
id, id,
@@ -57,10 +57,10 @@ export function useDescriptions(): [
useMemo(() => { useMemo(() => {
return function DescriptionProvider(props: DescriptionProviderProps) { return function DescriptionProvider(props: DescriptionProviderProps) {
let register = useCallback((value: string) => { let register = useCallback((value: string) => {
setDescriptionIds(existing => [...existing, value]) setDescriptionIds((existing) => [...existing, value])
return () => return () =>
setDescriptionIds(existing => { setDescriptionIds((existing) => {
let clone = existing.slice() let clone = existing.slice()
let idx = clone.indexOf(value) let idx = clone.indexOf(value)
if (idx !== -1) clone.splice(idx, 1) if (idx !== -1) clone.splice(idx, 1)
@@ -143,7 +143,7 @@ describe('Rendering', () => {
Trigger Trigger
</button> </button>
<Dialog open={isOpen} onClose={setIsOpen}> <Dialog open={isOpen} onClose={setIsOpen}>
{data => ( {(data) => (
<> <>
<pre>{JSON.stringify(data)}</pre> <pre>{JSON.stringify(data)}</pre>
<TabSentinel /> <TabSentinel />
@@ -204,7 +204,7 @@ describe('Rendering', () => {
return ( return (
<> <>
<button id="trigger" onClick={() => setIsOpen(v => !v)}> <button id="trigger" onClick={() => setIsOpen((v) => !v)}>
Trigger Trigger
</button> </button>
<Dialog open={isOpen} onClose={setIsOpen} unmount={false}> <Dialog open={isOpen} onClose={setIsOpen} unmount={false}>
@@ -239,7 +239,7 @@ describe('Rendering', () => {
return ( return (
<> <>
<button id="trigger" onClick={() => setIsOpen(v => !v)}> <button id="trigger" onClick={() => setIsOpen((v) => !v)}>
Trigger Trigger
</button> </button>
@@ -277,7 +277,7 @@ describe('Rendering', () => {
let [isOpen, setIsOpen] = useState(false) let [isOpen, setIsOpen] = useState(false)
return ( return (
<> <>
<button id="trigger" onClick={() => setIsOpen(v => !v)}> <button id="trigger" onClick={() => setIsOpen((v) => !v)}>
Trigger Trigger
</button> </button>
<Dialog open={isOpen} onClose={setIsOpen}> <Dialog open={isOpen} onClose={setIsOpen}>
@@ -400,7 +400,7 @@ describe('Keyboard interactions', () => {
let [isOpen, setIsOpen] = useState(false) let [isOpen, setIsOpen] = useState(false)
return ( return (
<> <>
<button id="trigger" onClick={() => setIsOpen(v => !v)}> <button id="trigger" onClick={() => setIsOpen((v) => !v)}>
Trigger Trigger
</button> </button>
<Dialog open={isOpen} onClose={setIsOpen}> <Dialog open={isOpen} onClose={setIsOpen}>
@@ -438,7 +438,7 @@ describe('Keyboard interactions', () => {
let [isOpen, setIsOpen] = useState(false) let [isOpen, setIsOpen] = useState(false)
return ( return (
<> <>
<button id="trigger" onClick={() => setIsOpen(v => !v)}> <button id="trigger" onClick={() => setIsOpen((v) => !v)}>
Trigger Trigger
</button> </button>
<Dialog open={isOpen} onClose={setIsOpen}> <Dialog open={isOpen} onClose={setIsOpen}>
@@ -477,14 +477,14 @@ describe('Keyboard interactions', () => {
let [isOpen, setIsOpen] = useState(false) let [isOpen, setIsOpen] = useState(false)
return ( return (
<> <>
<button id="trigger" onClick={() => setIsOpen(v => !v)}> <button id="trigger" onClick={() => setIsOpen((v) => !v)}>
Trigger Trigger
</button> </button>
<Dialog open={isOpen} onClose={setIsOpen}> <Dialog open={isOpen} onClose={setIsOpen}>
Contents Contents
<input <input
id="name" id="name"
onKeyDown={event => { onKeyDown={(event) => {
event.preventDefault() event.preventDefault()
event.stopPropagation() event.stopPropagation()
}} }}
@@ -525,7 +525,7 @@ describe('Mouse interactions', () => {
let [isOpen, setIsOpen] = useState(false) let [isOpen, setIsOpen] = useState(false)
return ( return (
<> <>
<button id="trigger" onClick={() => setIsOpen(v => !v)}> <button id="trigger" onClick={() => setIsOpen((v) => !v)}>
Trigger Trigger
</button> </button>
<Dialog open={isOpen} onClose={setIsOpen}> <Dialog open={isOpen} onClose={setIsOpen}>
@@ -559,7 +559,7 @@ describe('Mouse interactions', () => {
let [isOpen, setIsOpen] = useState(false) let [isOpen, setIsOpen] = useState(false)
return ( return (
<> <>
<button id="trigger" onClick={() => setIsOpen(v => !v)}> <button id="trigger" onClick={() => setIsOpen((v) => !v)}>
Trigger Trigger
</button> </button>
<Dialog open={isOpen} onClose={setIsOpen}> <Dialog open={isOpen} onClose={setIsOpen}>
@@ -595,7 +595,7 @@ describe('Mouse interactions', () => {
let [isOpen, setIsOpen] = useState(false) let [isOpen, setIsOpen] = useState(false)
return ( return (
<> <>
<button onClick={() => setIsOpen(v => !v)}>Trigger</button> <button onClick={() => setIsOpen((v) => !v)}>Trigger</button>
<Dialog open={isOpen} onClose={setIsOpen}> <Dialog open={isOpen} onClose={setIsOpen}>
Contents Contents
<TabSentinel /> <TabSentinel />
@@ -630,7 +630,7 @@ describe('Mouse interactions', () => {
return ( return (
<> <>
<button>Hello</button> <button>Hello</button>
<button onClick={() => setIsOpen(v => !v)}>Trigger</button> <button onClick={() => setIsOpen((v) => !v)}>Trigger</button>
<Dialog open={isOpen} onClose={setIsOpen}> <Dialog open={isOpen} onClose={setIsOpen}>
Contents Contents
<TabSentinel /> <TabSentinel />
@@ -206,7 +206,7 @@ let DialogRoot = forwardRefWithAs(function Dialog<
useInertOthers(internalDialogRef, hasNestedDialogs ? enabled : false) useInertOthers(internalDialogRef, hasNestedDialogs ? enabled : false)
// Handle outside click // Handle outside click
useWindowEvent('mousedown', event => { useWindowEvent('mousedown', (event) => {
let target = event.target as HTMLElement let target = event.target as HTMLElement
if (dialogState !== DialogStates.Open) return if (dialogState !== DialogStates.Open) return
@@ -217,7 +217,7 @@ let DialogRoot = forwardRefWithAs(function Dialog<
}) })
// Handle `Escape` to close // Handle `Escape` to close
useWindowEvent('keydown', event => { useWindowEvent('keydown', (event) => {
if (event.key !== Keys.Escape) return if (event.key !== Keys.Escape) return
if (dialogState !== DialogStates.Open) return if (dialogState !== DialogStates.Open) return
if (hasNestedDialogs) return if (hasNestedDialogs) return
@@ -250,7 +250,7 @@ let DialogRoot = forwardRefWithAs(function Dialog<
if (dialogState !== DialogStates.Open) return if (dialogState !== DialogStates.Open) return
if (!internalDialogRef.current) return if (!internalDialogRef.current) return
let observer = new IntersectionObserver(entries => { let observer = new IntersectionObserver((entries) => {
for (let entry of entries) { for (let entry of entries) {
if ( if (
entry.boundingClientRect.x === 0 && entry.boundingClientRect.x === 0 &&
@@ -277,9 +277,10 @@ let DialogRoot = forwardRefWithAs(function Dialog<
[dialogState, state, close, setTitleId] [dialogState, state, close, setTitleId]
) )
let slot = useMemo<DialogRenderPropArg>(() => ({ open: dialogState === DialogStates.Open }), [ let slot = useMemo<DialogRenderPropArg>(
dialogState, () => ({ open: dialogState === DialogStates.Open }),
]) [dialogState]
)
let propsWeControl = { let propsWeControl = {
ref: dialogRef, ref: dialogRef,
@@ -304,11 +305,11 @@ let DialogRoot = forwardRefWithAs(function Dialog<
match(message, { match(message, {
[StackMessage.Add]() { [StackMessage.Add]() {
containers.current.add(element) containers.current.add(element)
setNestedDialogCount(count => count + 1) setNestedDialogCount((count) => count + 1)
}, },
[StackMessage.Remove]() { [StackMessage.Remove]() {
containers.current.add(element) containers.current.add(element)
setNestedDialogCount(count => count - 1) setNestedDialogCount((count) => count - 1)
}, },
}) })
}, [])} }, [])}
@@ -348,7 +349,7 @@ type OverlayPropsWeControl = 'id' | 'aria-hidden' | 'onClick'
let Overlay = forwardRefWithAs(function Overlay< let Overlay = forwardRefWithAs(function Overlay<
TTag extends ElementType = typeof DEFAULT_OVERLAY_TAG TTag extends ElementType = typeof DEFAULT_OVERLAY_TAG
>(props: Props<TTag, OverlayRenderPropArg, OverlayPropsWeControl>, ref: Ref<HTMLDivElement>) { >(props: Props<TTag, OverlayRenderPropArg, OverlayPropsWeControl>, ref: Ref<HTMLDivElement>) {
let [{ dialogState, close }] = useDialogContext([Dialog.displayName, Overlay.name].join('.')) let [{ dialogState, close }] = useDialogContext('Dialog.Overlay')
let overlayRef = useSyncRefs(ref) let overlayRef = useSyncRefs(ref)
let id = `headlessui-dialog-overlay-${useId()}` let id = `headlessui-dialog-overlay-${useId()}`
@@ -364,9 +365,10 @@ let Overlay = forwardRefWithAs(function Overlay<
[close] [close]
) )
let slot = useMemo<OverlayRenderPropArg>(() => ({ open: dialogState === DialogStates.Open }), [ let slot = useMemo<OverlayRenderPropArg>(
dialogState, () => ({ open: dialogState === DialogStates.Open }),
]) [dialogState]
)
let propsWeControl = { let propsWeControl = {
ref: overlayRef, ref: overlayRef,
id, id,
@@ -394,7 +396,7 @@ type TitlePropsWeControl = 'id'
function Title<TTag extends ElementType = typeof DEFAULT_TITLE_TAG>( function Title<TTag extends ElementType = typeof DEFAULT_TITLE_TAG>(
props: Props<TTag, TitleRenderPropArg, TitlePropsWeControl> props: Props<TTag, TitleRenderPropArg, TitlePropsWeControl>
) { ) {
let [{ dialogState, setTitleId }] = useDialogContext([Dialog.displayName, Title.name].join('.')) let [{ dialogState, setTitleId }] = useDialogContext('Dialog.Title')
let id = `headlessui-dialog-title-${useId()}` let id = `headlessui-dialog-title-${useId()}`
@@ -403,9 +405,10 @@ function Title<TTag extends ElementType = typeof DEFAULT_TITLE_TAG>(
return () => setTitleId(null) return () => setTitleId(null)
}, [id, setTitleId]) }, [id, setTitleId])
let slot = useMemo<TitleRenderPropArg>(() => ({ open: dialogState === DialogStates.Open }), [ let slot = useMemo<TitleRenderPropArg>(
dialogState, () => ({ open: dialogState === DialogStates.Open }),
]) [dialogState]
)
let propsWeControl = { id } let propsWeControl = { id }
let passthroughProps = props let passthroughProps = props
@@ -20,7 +20,7 @@ jest.mock('../../hooks/use-id')
afterAll(() => jest.restoreAllMocks()) afterAll(() => jest.restoreAllMocks())
function nextFrame() { function nextFrame() {
return new Promise<void>(resolve => { return new Promise<void>((resolve) => {
requestAnimationFrame(() => { requestAnimationFrame(() => {
requestAnimationFrame(() => { requestAnimationFrame(() => {
resolve() resolve()
@@ -68,14 +68,14 @@ let reducers: {
action: Extract<Actions, { type: P }> action: Extract<Actions, { type: P }>
) => StateDefinition ) => StateDefinition
} = { } = {
[ActionTypes.ToggleDisclosure]: state => ({ [ActionTypes.ToggleDisclosure]: (state) => ({
...state, ...state,
disclosureState: match(state.disclosureState, { disclosureState: match(state.disclosureState, {
[DisclosureStates.Open]: DisclosureStates.Closed, [DisclosureStates.Open]: DisclosureStates.Closed,
[DisclosureStates.Closed]: DisclosureStates.Open, [DisclosureStates.Closed]: DisclosureStates.Open,
}), }),
}), }),
[ActionTypes.CloseDisclosure]: state => { [ActionTypes.CloseDisclosure]: (state) => {
if (state.disclosureState === DisclosureStates.Closed) return state if (state.disclosureState === DisclosureStates.Closed) return state
return { ...state, disclosureState: DisclosureStates.Closed } return { ...state, disclosureState: DisclosureStates.Closed }
}, },
@@ -227,7 +227,7 @@ let Button = forwardRefWithAs(function Button<TTag extends ElementType = typeof
props: Props<TTag, ButtonRenderPropArg, ButtonPropsWeControl>, props: Props<TTag, ButtonRenderPropArg, ButtonPropsWeControl>,
ref: Ref<HTMLButtonElement> ref: Ref<HTMLButtonElement>
) { ) {
let [state, dispatch] = useDisclosureContext([Disclosure.name, Button.name].join('.')) let [state, dispatch] = useDisclosureContext('Disclosure.Button')
let internalButtonRef = useRef<HTMLButtonElement | null>(null) let internalButtonRef = useRef<HTMLButtonElement | null>(null)
let buttonRef = useSyncRefs(internalButtonRef, ref) let buttonRef = useSyncRefs(internalButtonRef, ref)
@@ -334,8 +334,8 @@ let Panel = forwardRefWithAs(function Panel<TTag extends ElementType = typeof DE
PropsForFeatures<typeof PanelRenderFeatures>, PropsForFeatures<typeof PanelRenderFeatures>,
ref: Ref<HTMLDivElement> ref: Ref<HTMLDivElement>
) { ) {
let [state, dispatch] = useDisclosureContext([Disclosure.name, Panel.name].join('.')) let [state, dispatch] = useDisclosureContext('Disclosure.Panel')
let { close } = useDisclosureAPIContext([Disclosure.name, Panel.name].join('.')) let { close } = useDisclosureAPIContext('Disclosure.Panel')
let panelRef = useSyncRefs(ref, () => { let panelRef = useSyncRefs(ref, () => {
if (state.linkedPanel) return if (state.linkedPanel) return
@@ -52,10 +52,10 @@ export function useLabels(): [string | undefined, (props: LabelProviderProps) =>
useMemo(() => { useMemo(() => {
return function LabelProvider(props: LabelProviderProps) { return function LabelProvider(props: LabelProviderProps) {
let register = useCallback((value: string) => { let register = useCallback((value: string) => {
setLabelIds(existing => [...existing, value]) setLabelIds((existing) => [...existing, value])
return () => return () =>
setLabelIds(existing => { setLabelIds((existing) => {
let clone = existing.slice() let clone = existing.slice()
let idx = clone.indexOf(value) let idx = clone.indexOf(value)
if (idx !== -1) clone.splice(idx, 1) if (idx !== -1) clone.splice(idx, 1)
@@ -56,6 +56,7 @@ describe('safeguards', () => {
])( ])(
'should error when we are using a <%s /> without a parent <Listbox />', 'should error when we are using a <%s /> without a parent <Listbox />',
suppressConsoleLogs((name, Component) => { suppressConsoleLogs((name, Component) => {
// @ts-expect-error This is fine
expect(() => render(createElement(Component))).toThrowError( expect(() => render(createElement(Component))).toThrowError(
`<${name} /> is missing a parent <Listbox /> component.` `<${name} /> is missing a parent <Listbox /> component.`
) )
@@ -396,7 +397,7 @@ describe('Rendering', () => {
<Listbox value={undefined} onChange={console.log}> <Listbox value={undefined} onChange={console.log}>
<Listbox.Button>Trigger</Listbox.Button> <Listbox.Button>Trigger</Listbox.Button>
<Listbox.Options> <Listbox.Options>
{data => ( {(data) => (
<> <>
<Listbox.Option value="a">{JSON.stringify(data)}</Listbox.Option> <Listbox.Option value="a">{JSON.stringify(data)}</Listbox.Option>
</> </>
@@ -547,10 +548,10 @@ describe('Rendering composition', () => {
<Listbox value={undefined} onChange={console.log}> <Listbox value={undefined} onChange={console.log}>
<Listbox.Button>Trigger</Listbox.Button> <Listbox.Button>Trigger</Listbox.Button>
<Listbox.Options> <Listbox.Options>
<Listbox.Option value="a" className={bag => JSON.stringify(bag)}> <Listbox.Option value="a" className={(bag) => JSON.stringify(bag)}>
Option A Option A
</Listbox.Option> </Listbox.Option>
<Listbox.Option value="b" disabled className={bag => JSON.stringify(bag)}> <Listbox.Option value="b" disabled className={(bag) => JSON.stringify(bag)}>
Option B Option B
</Listbox.Option> </Listbox.Option>
<Listbox.Option value="c" className="no-special-treatment"> <Listbox.Option value="c" className="no-special-treatment">
@@ -645,7 +646,7 @@ describe('Rendering composition', () => {
await click(getListboxButton()) await click(getListboxButton())
// Verify options are buttons now // Verify options are buttons now
getListboxOptions().forEach(option => assertListboxOption(option, { tag: 'button' })) getListboxOptions().forEach((option) => assertListboxOption(option, { tag: 'button' }))
}) })
) )
}) })
@@ -673,7 +674,7 @@ describe('Composition', () => {
<Debug name="Transition" fn={orderFn} /> <Debug name="Transition" fn={orderFn} />
<Listbox.Options> <Listbox.Options>
<Listbox.Option value="a"> <Listbox.Option value="a">
{data => ( {(data) => (
<> <>
{JSON.stringify(data)} {JSON.stringify(data)}
<Debug name="Listbox.Option" fn={orderFn} /> <Debug name="Listbox.Option" fn={orderFn} />
@@ -756,7 +757,7 @@ describe('Keyboard interactions', () => {
// Verify we have listbox options // Verify we have listbox options
let options = getListboxOptions() let options = getListboxOptions()
expect(options).toHaveLength(3) expect(options).toHaveLength(3)
options.forEach(option => assertListboxOption(option, { selected: false })) options.forEach((option) => assertListboxOption(option, { selected: false }))
// Verify that the first listbox option is active // Verify that the first listbox option is active
assertActiveListboxOption(options[0]) assertActiveListboxOption(options[0])
@@ -918,7 +919,7 @@ describe('Keyboard interactions', () => {
<Listbox value={selectedOption} onChange={console.log}> <Listbox value={selectedOption} onChange={console.log}>
<Listbox.Button>Trigger</Listbox.Button> <Listbox.Button>Trigger</Listbox.Button>
<Listbox.Options> <Listbox.Options>
{myOptions.map(myOption => ( {myOptions.map((myOption) => (
<Listbox.Option key={myOption.id} value={myOption}> <Listbox.Option key={myOption.id} value={myOption}>
{myOption.name} {myOption.name}
</Listbox.Option> </Listbox.Option>
@@ -1139,7 +1140,7 @@ describe('Keyboard interactions', () => {
return ( return (
<Listbox <Listbox
value={value} value={value}
onChange={value => { onChange={(value) => {
setValue(value) setValue(value)
handleChange(value) handleChange(value)
}} }}
@@ -1234,7 +1235,7 @@ describe('Keyboard interactions', () => {
// Verify we have listbox options // Verify we have listbox options
let options = getListboxOptions() let options = getListboxOptions()
expect(options).toHaveLength(3) expect(options).toHaveLength(3)
options.forEach(option => assertListboxOption(option)) options.forEach((option) => assertListboxOption(option))
assertActiveListboxOption(options[0]) assertActiveListboxOption(options[0])
}) })
) )
@@ -1462,7 +1463,7 @@ describe('Keyboard interactions', () => {
return ( return (
<Listbox <Listbox
value={value} value={value}
onChange={value => { onChange={(value) => {
setValue(value) setValue(value)
handleChange(value) handleChange(value)
}} }}
@@ -1600,7 +1601,7 @@ describe('Keyboard interactions', () => {
// Verify we have listbox options // Verify we have listbox options
let options = getListboxOptions() let options = getListboxOptions()
expect(options).toHaveLength(3) expect(options).toHaveLength(3)
options.forEach(option => assertListboxOption(option)) options.forEach((option) => assertListboxOption(option))
assertActiveListboxOption(options[0]) assertActiveListboxOption(options[0])
// Try to tab // Try to tab
@@ -1651,7 +1652,7 @@ describe('Keyboard interactions', () => {
// Verify we have listbox options // Verify we have listbox options
let options = getListboxOptions() let options = getListboxOptions()
expect(options).toHaveLength(3) expect(options).toHaveLength(3)
options.forEach(option => assertListboxOption(option)) options.forEach((option) => assertListboxOption(option))
assertActiveListboxOption(options[0]) assertActiveListboxOption(options[0])
// Try to Shift+Tab // Try to Shift+Tab
@@ -1704,7 +1705,7 @@ describe('Keyboard interactions', () => {
// Verify we have listbox options // Verify we have listbox options
let options = getListboxOptions() let options = getListboxOptions()
expect(options).toHaveLength(3) expect(options).toHaveLength(3)
options.forEach(option => assertListboxOption(option)) options.forEach((option) => assertListboxOption(option))
// Verify that the first listbox option is active // Verify that the first listbox option is active
assertActiveListboxOption(options[0]) assertActiveListboxOption(options[0])
@@ -1844,7 +1845,7 @@ describe('Keyboard interactions', () => {
// Verify we have listbox options // Verify we have listbox options
let options = getListboxOptions() let options = getListboxOptions()
expect(options).toHaveLength(3) expect(options).toHaveLength(3)
options.forEach(option => assertListboxOption(option)) options.forEach((option) => assertListboxOption(option))
assertActiveListboxOption(options[0]) assertActiveListboxOption(options[0])
// We should be able to go down once // We should be able to go down once
@@ -1892,7 +1893,7 @@ describe('Keyboard interactions', () => {
// Verify we have listbox options // Verify we have listbox options
let options = getListboxOptions() let options = getListboxOptions()
expect(options).toHaveLength(3) expect(options).toHaveLength(3)
options.forEach(option => assertListboxOption(option)) options.forEach((option) => assertListboxOption(option))
assertActiveListboxOption(options[1]) assertActiveListboxOption(options[1])
// We should be able to go down once // We should be able to go down once
@@ -1934,7 +1935,7 @@ describe('Keyboard interactions', () => {
// Verify we have listbox options // Verify we have listbox options
let options = getListboxOptions() let options = getListboxOptions()
expect(options).toHaveLength(3) expect(options).toHaveLength(3)
options.forEach(option => assertListboxOption(option)) options.forEach((option) => assertListboxOption(option))
assertActiveListboxOption(options[2]) assertActiveListboxOption(options[2])
}) })
) )
@@ -1970,7 +1971,7 @@ describe('Keyboard interactions', () => {
// Verify we have listbox options // Verify we have listbox options
let options = getListboxOptions() let options = getListboxOptions()
expect(options).toHaveLength(3) expect(options).toHaveLength(3)
options.forEach(option => assertListboxOption(option)) options.forEach((option) => assertListboxOption(option))
assertActiveListboxOption(options[0]) assertActiveListboxOption(options[0])
// We should be able to go right once // We should be able to go right once
@@ -2027,7 +2028,7 @@ describe('Keyboard interactions', () => {
// Verify we have listbox options // Verify we have listbox options
let options = getListboxOptions() let options = getListboxOptions()
expect(options).toHaveLength(3) expect(options).toHaveLength(3)
options.forEach(option => assertListboxOption(option)) options.forEach((option) => assertListboxOption(option))
// ! ALERT: The LAST option should now be active // ! ALERT: The LAST option should now be active
assertActiveListboxOption(options[2]) assertActiveListboxOption(options[2])
@@ -2171,7 +2172,7 @@ describe('Keyboard interactions', () => {
// Verify we have listbox options // Verify we have listbox options
let options = getListboxOptions() let options = getListboxOptions()
expect(options).toHaveLength(3) expect(options).toHaveLength(3)
options.forEach(option => assertListboxOption(option)) options.forEach((option) => assertListboxOption(option))
assertActiveListboxOption(options[0]) assertActiveListboxOption(options[0])
}) })
) )
@@ -2209,7 +2210,7 @@ describe('Keyboard interactions', () => {
// Verify we have listbox options // Verify we have listbox options
let options = getListboxOptions() let options = getListboxOptions()
expect(options).toHaveLength(3) expect(options).toHaveLength(3)
options.forEach(option => assertListboxOption(option)) options.forEach((option) => assertListboxOption(option))
assertActiveListboxOption(options[2]) assertActiveListboxOption(options[2])
// We should not be able to go up (because those are disabled) // We should not be able to go up (because those are disabled)
@@ -2260,7 +2261,7 @@ describe('Keyboard interactions', () => {
// Verify we have listbox options // Verify we have listbox options
let options = getListboxOptions() let options = getListboxOptions()
expect(options).toHaveLength(3) expect(options).toHaveLength(3)
options.forEach(option => assertListboxOption(option)) options.forEach((option) => assertListboxOption(option))
assertActiveListboxOption(options[2]) assertActiveListboxOption(options[2])
// We should be able to go down once // We should be able to go down once
@@ -2318,7 +2319,7 @@ describe('Keyboard interactions', () => {
// Verify we have listbox options // Verify we have listbox options
let options = getListboxOptions() let options = getListboxOptions()
expect(options).toHaveLength(3) expect(options).toHaveLength(3)
options.forEach(option => assertListboxOption(option)) options.forEach((option) => assertListboxOption(option))
assertActiveListboxOption(options[2]) assertActiveListboxOption(options[2])
// We should be able to go left once // We should be able to go left once
@@ -3198,7 +3199,7 @@ describe('Mouse interactions', () => {
// Verify we have listbox options // Verify we have listbox options
let options = getListboxOptions() let options = getListboxOptions()
expect(options).toHaveLength(3) expect(options).toHaveLength(3)
options.forEach(option => assertListboxOption(option)) options.forEach((option) => assertListboxOption(option))
}) })
) )
@@ -3726,7 +3727,7 @@ describe('Mouse interactions', () => {
return ( return (
<Listbox <Listbox
value={value} value={value}
onChange={value => { onChange={(value) => {
setValue(value) setValue(value)
handleChange(value) handleChange(value)
}} }}
@@ -3777,7 +3778,7 @@ describe('Mouse interactions', () => {
return ( return (
<Listbox <Listbox
value={value} value={value}
onChange={value => { onChange={(value) => {
setValue(value) setValue(value)
handleChange(value) handleChange(value)
}} }}
@@ -119,8 +119,8 @@ let reducers: {
let activeOptionIndex = calculateActiveIndex(action, { let activeOptionIndex = calculateActiveIndex(action, {
resolveItems: () => state.options, resolveItems: () => state.options,
resolveActiveIndex: () => state.activeOptionIndex, resolveActiveIndex: () => state.activeOptionIndex,
resolveId: item => item.id, resolveId: (item) => item.id,
resolveDisabled: item => item.dataRef.current.disabled, resolveDisabled: (item) => item.dataRef.current.disabled,
}) })
if (state.searchQuery === '' && state.activeOptionIndex === activeOptionIndex) return state if (state.searchQuery === '' && state.activeOptionIndex === activeOptionIndex) return state
@@ -140,7 +140,7 @@ let reducers: {
: state.options : state.options
let matchingOption = reOrderedOptions.find( let matchingOption = reOrderedOptions.find(
option => (option) =>
!option.dataRef.current.disabled && !option.dataRef.current.disabled &&
option.dataRef.current.textValue?.startsWith(searchQuery) option.dataRef.current.textValue?.startsWith(searchQuery)
) )
@@ -175,7 +175,7 @@ let reducers: {
let currentActiveOption = let currentActiveOption =
state.activeOptionIndex !== null ? nextOptions[state.activeOptionIndex] : null state.activeOptionIndex !== null ? nextOptions[state.activeOptionIndex] : null
let idx = nextOptions.findIndex(a => a.id === action.id) let idx = nextOptions.findIndex((a) => a.id === action.id)
if (idx !== -1) nextOptions.splice(idx, 1) if (idx !== -1) nextOptions.splice(idx, 1)
@@ -251,12 +251,13 @@ export function Listbox<TTag extends ElementType = typeof DEFAULT_LISTBOX_TAG, T
propsRef.current.onChange = onChange propsRef.current.onChange = onChange
}, [onChange, propsRef]) }, [onChange, propsRef])
useIsoMorphicEffect(() => dispatch({ type: ActionTypes.SetDisabled, disabled }), [disabled]) useIsoMorphicEffect(() => dispatch({ type: ActionTypes.SetDisabled, disabled }), [disabled])
useIsoMorphicEffect(() => dispatch({ type: ActionTypes.SetOrientation, orientation }), [ useIsoMorphicEffect(
orientation, () => dispatch({ type: ActionTypes.SetOrientation, orientation }),
]) [orientation]
)
// Handle outside click // Handle outside click
useWindowEvent('mousedown', event => { useWindowEvent('mousedown', (event) => {
let target = event.target as HTMLElement let target = event.target as HTMLElement
if (listboxState !== ListboxStates.Open) return if (listboxState !== ListboxStates.Open) return
@@ -318,7 +319,7 @@ let Button = forwardRefWithAs(function Button<TTag extends ElementType = typeof
props: Props<TTag, ButtonRenderPropArg, ButtonPropsWeControl>, props: Props<TTag, ButtonRenderPropArg, ButtonPropsWeControl>,
ref: Ref<HTMLButtonElement> ref: Ref<HTMLButtonElement>
) { ) {
let [state, dispatch] = useListboxContext([Listbox.name, Button.name].join('.')) let [state, dispatch] = useListboxContext('Listbox.Button')
let buttonRef = useSyncRefs(state.buttonRef, ref) let buttonRef = useSyncRefs(state.buttonRef, ref)
let id = `headlessui-listbox-button-${useId()}` let id = `headlessui-listbox-button-${useId()}`
@@ -422,12 +423,13 @@ type LabelPropsWeControl = 'id' | 'ref' | 'onClick'
function Label<TTag extends ElementType = typeof DEFAULT_LABEL_TAG>( function Label<TTag extends ElementType = typeof DEFAULT_LABEL_TAG>(
props: Props<TTag, LabelRenderPropArg, LabelPropsWeControl> props: Props<TTag, LabelRenderPropArg, LabelPropsWeControl>
) { ) {
let [state] = useListboxContext([Listbox.name, Label.name].join('.')) let [state] = useListboxContext('Listbox.Label')
let id = `headlessui-listbox-label-${useId()}` let id = `headlessui-listbox-label-${useId()}`
let handleClick = useCallback(() => state.buttonRef.current?.focus({ preventScroll: true }), [ let handleClick = useCallback(
state.buttonRef, () => state.buttonRef.current?.focus({ preventScroll: true }),
]) [state.buttonRef]
)
let slot = useMemo<LabelRenderPropArg>( let slot = useMemo<LabelRenderPropArg>(
() => ({ open: state.listboxState === ListboxStates.Open, disabled: state.disabled }), () => ({ open: state.listboxState === ListboxStates.Open, disabled: state.disabled }),
@@ -466,7 +468,7 @@ let Options = forwardRefWithAs(function Options<
PropsForFeatures<typeof OptionsRenderFeatures>, PropsForFeatures<typeof OptionsRenderFeatures>,
ref: Ref<HTMLUListElement> ref: Ref<HTMLUListElement>
) { ) {
let [state, dispatch] = useListboxContext([Listbox.name, Options.name].join('.')) let [state, dispatch] = useListboxContext('Listbox.Options')
let optionsRef = useSyncRefs(state.optionsRef, ref) let optionsRef = useSyncRefs(state.optionsRef, ref)
let id = `headlessui-listbox-options-${useId()}` let id = `headlessui-listbox-options-${useId()}`
@@ -561,10 +563,10 @@ let Options = forwardRefWithAs(function Options<
[d, dispatch, searchDisposables, state] [d, dispatch, searchDisposables, state]
) )
let labelledby = useComputed(() => state.labelRef.current?.id ?? state.buttonRef.current?.id, [ let labelledby = useComputed(
state.labelRef.current, () => state.labelRef.current?.id ?? state.buttonRef.current?.id,
state.buttonRef.current, [state.labelRef.current, state.buttonRef.current]
]) )
let slot = useMemo<OptionsRenderPropArg>( let slot = useMemo<OptionsRenderPropArg>(
() => ({ open: state.listboxState === ListboxStates.Open }), () => ({ open: state.listboxState === ListboxStates.Open }),
@@ -625,7 +627,7 @@ function Option<
} }
) { ) {
let { disabled = false, value, ...passthroughProps } = props let { disabled = false, value, ...passthroughProps } = props
let [state, dispatch] = useListboxContext([Listbox.name, Option.name].join('.')) let [state, dispatch] = useListboxContext('Listbox.Option')
let id = `headlessui-listbox-option-${useId()}` let id = `headlessui-listbox-option-${useId()}`
let active = let active =
state.activeOptionIndex !== null ? state.options[state.activeOptionIndex].id === id : false state.activeOptionIndex !== null ? state.options[state.activeOptionIndex].id === id : false
@@ -692,11 +694,10 @@ function Option<
dispatch({ type: ActionTypes.GoToOption, focus: Focus.Nothing }) dispatch({ type: ActionTypes.GoToOption, focus: Focus.Nothing })
}, [disabled, active, dispatch]) }, [disabled, active, dispatch])
let slot = useMemo<OptionRenderPropArg>(() => ({ active, selected, disabled }), [ let slot = useMemo<OptionRenderPropArg>(
active, () => ({ active, selected, disabled }),
selected, [active, selected, disabled]
disabled, )
])
let propsWeControl = { let propsWeControl = {
id, id,
role: 'option', role: 'option',
@@ -253,7 +253,7 @@ describe('Rendering', () => {
<Menu> <Menu>
<Menu.Button>Trigger</Menu.Button> <Menu.Button>Trigger</Menu.Button>
<Menu.Items> <Menu.Items>
{data => ( {(data) => (
<> <>
<Menu.Item as="a">{JSON.stringify(data)}</Menu.Item> <Menu.Item as="a">{JSON.stringify(data)}</Menu.Item>
</> </>
@@ -403,10 +403,10 @@ describe('Rendering composition', () => {
<Menu> <Menu>
<Menu.Button>Trigger</Menu.Button> <Menu.Button>Trigger</Menu.Button>
<Menu.Items> <Menu.Items>
<Menu.Item as="a" className={bag => JSON.stringify(bag)}> <Menu.Item as="a" className={(bag) => JSON.stringify(bag)}>
Item A Item A
</Menu.Item> </Menu.Item>
<Menu.Item as="a" disabled className={bag => JSON.stringify(bag)}> <Menu.Item as="a" disabled className={(bag) => JSON.stringify(bag)}>
Item B Item B
</Menu.Item> </Menu.Item>
<Menu.Item as="a" className="no-special-treatment"> <Menu.Item as="a" className="no-special-treatment">
@@ -484,7 +484,7 @@ describe('Rendering composition', () => {
// Verify items are buttons now // Verify items are buttons now
let items = getMenuItems() let items = getMenuItems()
items.forEach(item => assertMenuItem(item, { tag: 'button' })) items.forEach((item) => assertMenuItem(item, { tag: 'button' }))
}) })
) )
@@ -496,11 +496,11 @@ describe('Rendering composition', () => {
<Menu.Button>Trigger</Menu.Button> <Menu.Button>Trigger</Menu.Button>
<div className="outer"> <div className="outer">
<Menu.Items> <Menu.Items>
<div className="py-1 inner"> <div className="inner py-1">
<Menu.Item as="button">Item A</Menu.Item> <Menu.Item as="button">Item A</Menu.Item>
<Menu.Item as="button">Item B</Menu.Item> <Menu.Item as="button">Item B</Menu.Item>
</div> </div>
<div className="py-1 inner"> <div className="inner py-1">
<Menu.Item as="button">Item C</Menu.Item> <Menu.Item as="button">Item C</Menu.Item>
<Menu.Item> <Menu.Item>
<div> <div>
@@ -508,7 +508,7 @@ describe('Rendering composition', () => {
</div> </div>
</Menu.Item> </Menu.Item>
</div> </div>
<div className="py-1 inner"> <div className="inner py-1">
<form className="inner"> <form className="inner">
<Menu.Item as="button">Item E</Menu.Item> <Menu.Item as="button">Item E</Menu.Item>
</form> </form>
@@ -523,11 +523,11 @@ describe('Rendering composition', () => {
expect.hasAssertions() expect.hasAssertions()
document.querySelectorAll('.outer').forEach(element => { document.querySelectorAll('.outer').forEach((element) => {
expect(element).not.toHaveAttribute('role', 'none') expect(element).not.toHaveAttribute('role', 'none')
}) })
document.querySelectorAll('.inner').forEach(element => { document.querySelectorAll('.inner').forEach((element) => {
expect(element).toHaveAttribute('role', 'none') expect(element).toHaveAttribute('role', 'none')
}) })
}) })
@@ -557,7 +557,7 @@ describe('Composition', () => {
<Debug name="Transition" fn={orderFn} /> <Debug name="Transition" fn={orderFn} />
<Menu.Items> <Menu.Items>
<Menu.Item as="a"> <Menu.Item as="a">
{data => ( {(data) => (
<> <>
{JSON.stringify(data)} {JSON.stringify(data)}
<Debug name="Menu.Item" fn={orderFn} /> <Debug name="Menu.Item" fn={orderFn} />
@@ -611,7 +611,7 @@ describe('Composition', () => {
<Debug name="Transition" fn={orderFn} /> <Debug name="Transition" fn={orderFn} />
<Menu.Items> <Menu.Items>
<Menu.Item as="a"> <Menu.Item as="a">
{data => ( {(data) => (
<> <>
{JSON.stringify(data)} {JSON.stringify(data)}
<Debug name="Menu.Item" fn={orderFn} /> <Debug name="Menu.Item" fn={orderFn} />
@@ -693,7 +693,7 @@ describe('Keyboard interactions', () => {
// Verify we have menu items // Verify we have menu items
let items = getMenuItems() let items = getMenuItems()
expect(items).toHaveLength(3) expect(items).toHaveLength(3)
items.forEach(item => assertMenuItem(item)) items.forEach((item) => assertMenuItem(item))
// Verify that the first menu item is active // Verify that the first menu item is active
assertMenuLinkedWithMenuItem(items[0]) assertMenuLinkedWithMenuItem(items[0])
@@ -1057,7 +1057,7 @@ describe('Keyboard interactions', () => {
// Verify we have menu items // Verify we have menu items
let items = getMenuItems() let items = getMenuItems()
expect(items).toHaveLength(3) expect(items).toHaveLength(3)
items.forEach(item => assertMenuItem(item)) items.forEach((item) => assertMenuItem(item))
assertMenuLinkedWithMenuItem(items[0]) assertMenuLinkedWithMenuItem(items[0])
}) })
) )
@@ -1395,7 +1395,7 @@ describe('Keyboard interactions', () => {
// Verify we have menu items // Verify we have menu items
let items = getMenuItems() let items = getMenuItems()
expect(items).toHaveLength(3) expect(items).toHaveLength(3)
items.forEach(item => assertMenuItem(item)) items.forEach((item) => assertMenuItem(item))
assertMenuLinkedWithMenuItem(items[0]) assertMenuLinkedWithMenuItem(items[0])
// Try to tab // Try to tab
@@ -1444,7 +1444,7 @@ describe('Keyboard interactions', () => {
// Verify we have menu items // Verify we have menu items
let items = getMenuItems() let items = getMenuItems()
expect(items).toHaveLength(3) expect(items).toHaveLength(3)
items.forEach(item => assertMenuItem(item)) items.forEach((item) => assertMenuItem(item))
assertMenuLinkedWithMenuItem(items[0]) assertMenuLinkedWithMenuItem(items[0])
// Try to Shift+Tab // Try to Shift+Tab
@@ -1495,7 +1495,7 @@ describe('Keyboard interactions', () => {
// Verify we have menu items // Verify we have menu items
let items = getMenuItems() let items = getMenuItems()
expect(items).toHaveLength(3) expect(items).toHaveLength(3)
items.forEach(item => assertMenuItem(item)) items.forEach((item) => assertMenuItem(item))
// Verify that the first menu item is active // Verify that the first menu item is active
assertMenuLinkedWithMenuItem(items[0]) assertMenuLinkedWithMenuItem(items[0])
@@ -1589,7 +1589,7 @@ describe('Keyboard interactions', () => {
// Verify we have menu items // Verify we have menu items
let items = getMenuItems() let items = getMenuItems()
expect(items).toHaveLength(3) expect(items).toHaveLength(3)
items.forEach(item => assertMenuItem(item)) items.forEach((item) => assertMenuItem(item))
assertMenuLinkedWithMenuItem(items[0]) assertMenuLinkedWithMenuItem(items[0])
// We should be able to go down once // We should be able to go down once
@@ -1637,7 +1637,7 @@ describe('Keyboard interactions', () => {
// Verify we have menu items // Verify we have menu items
let items = getMenuItems() let items = getMenuItems()
expect(items).toHaveLength(3) expect(items).toHaveLength(3)
items.forEach(item => assertMenuItem(item)) items.forEach((item) => assertMenuItem(item))
assertMenuLinkedWithMenuItem(items[1]) assertMenuLinkedWithMenuItem(items[1])
// We should be able to go down once // We should be able to go down once
@@ -1679,7 +1679,7 @@ describe('Keyboard interactions', () => {
// Verify we have menu items // Verify we have menu items
let items = getMenuItems() let items = getMenuItems()
expect(items).toHaveLength(3) expect(items).toHaveLength(3)
items.forEach(item => assertMenuItem(item)) items.forEach((item) => assertMenuItem(item))
assertMenuLinkedWithMenuItem(items[2]) assertMenuLinkedWithMenuItem(items[2])
}) })
) )
@@ -1723,7 +1723,7 @@ describe('Keyboard interactions', () => {
// Verify we have menu items // Verify we have menu items
let items = getMenuItems() let items = getMenuItems()
expect(items).toHaveLength(3) expect(items).toHaveLength(3)
items.forEach(item => assertMenuItem(item)) items.forEach((item) => assertMenuItem(item))
// ! ALERT: The LAST item should now be active // ! ALERT: The LAST item should now be active
assertMenuLinkedWithMenuItem(items[2]) assertMenuLinkedWithMenuItem(items[2])
@@ -1821,7 +1821,7 @@ describe('Keyboard interactions', () => {
// Verify we have menu items // Verify we have menu items
let items = getMenuItems() let items = getMenuItems()
expect(items).toHaveLength(3) expect(items).toHaveLength(3)
items.forEach(item => assertMenuItem(item)) items.forEach((item) => assertMenuItem(item))
assertMenuLinkedWithMenuItem(items[0]) assertMenuLinkedWithMenuItem(items[0])
}) })
) )
@@ -1859,7 +1859,7 @@ describe('Keyboard interactions', () => {
// Verify we have menu items // Verify we have menu items
let items = getMenuItems() let items = getMenuItems()
expect(items).toHaveLength(3) expect(items).toHaveLength(3)
items.forEach(item => assertMenuItem(item)) items.forEach((item) => assertMenuItem(item))
assertMenuLinkedWithMenuItem(items[2]) assertMenuLinkedWithMenuItem(items[2])
// We should not be able to go up (because those are disabled) // We should not be able to go up (because those are disabled)
@@ -1909,7 +1909,7 @@ describe('Keyboard interactions', () => {
// Verify we have menu items // Verify we have menu items
let items = getMenuItems() let items = getMenuItems()
expect(items).toHaveLength(3) expect(items).toHaveLength(3)
items.forEach(item => assertMenuItem(item)) items.forEach((item) => assertMenuItem(item))
assertMenuLinkedWithMenuItem(items[2]) assertMenuLinkedWithMenuItem(items[2])
// We should be able to go down once // We should be able to go down once
@@ -2736,7 +2736,7 @@ describe('Mouse interactions', () => {
// Verify we have menu items // Verify we have menu items
let items = getMenuItems() let items = getMenuItems()
expect(items).toHaveLength(3) expect(items).toHaveLength(3)
items.forEach(item => assertMenuItem(item)) items.forEach((item) => assertMenuItem(item))
}) })
) )
@@ -91,8 +91,8 @@ let reducers: {
let activeItemIndex = calculateActiveIndex(action, { let activeItemIndex = calculateActiveIndex(action, {
resolveItems: () => state.items, resolveItems: () => state.items,
resolveActiveIndex: () => state.activeItemIndex, resolveActiveIndex: () => state.activeItemIndex,
resolveId: item => item.id, resolveId: (item) => item.id,
resolveDisabled: item => item.dataRef.current.disabled, resolveDisabled: (item) => item.dataRef.current.disabled,
}) })
if (state.searchQuery === '' && state.activeItemIndex === activeItemIndex) return state if (state.searchQuery === '' && state.activeItemIndex === activeItemIndex) return state
@@ -109,7 +109,7 @@ let reducers: {
: state.items : state.items
let matchingItem = reOrderedItems.find( let matchingItem = reOrderedItems.find(
item => (item) =>
item.dataRef.current.textValue?.startsWith(searchQuery) && !item.dataRef.current.disabled item.dataRef.current.textValue?.startsWith(searchQuery) && !item.dataRef.current.disabled
) )
@@ -139,7 +139,7 @@ let reducers: {
let nextItems = state.items.slice() let nextItems = state.items.slice()
let currentActiveItem = state.activeItemIndex !== null ? nextItems[state.activeItemIndex] : null let currentActiveItem = state.activeItemIndex !== null ? nextItems[state.activeItemIndex] : null
let idx = nextItems.findIndex(a => a.id === action.id) let idx = nextItems.findIndex((a) => a.id === action.id)
if (idx !== -1) nextItems.splice(idx, 1) if (idx !== -1) nextItems.splice(idx, 1)
@@ -196,7 +196,7 @@ export function Menu<TTag extends ElementType = typeof DEFAULT_MENU_TAG>(
let [{ menuState, itemsRef, buttonRef }, dispatch] = reducerBag let [{ menuState, itemsRef, buttonRef }, dispatch] = reducerBag
// Handle outside click // Handle outside click
useWindowEvent('mousedown', event => { useWindowEvent('mousedown', (event) => {
let target = event.target as HTMLElement let target = event.target as HTMLElement
if (menuState !== MenuStates.Open) return if (menuState !== MenuStates.Open) return
@@ -212,9 +212,10 @@ export function Menu<TTag extends ElementType = typeof DEFAULT_MENU_TAG>(
} }
}) })
let slot = useMemo<MenuRenderPropArg>(() => ({ open: menuState === MenuStates.Open }), [ let slot = useMemo<MenuRenderPropArg>(
menuState, () => ({ open: menuState === MenuStates.Open }),
]) [menuState]
)
return ( return (
<MenuContext.Provider value={reducerBag}> <MenuContext.Provider value={reducerBag}>
@@ -249,7 +250,7 @@ let Button = forwardRefWithAs(function Button<TTag extends ElementType = typeof
props: Props<TTag, ButtonRenderPropArg, ButtonPropsWeControl>, props: Props<TTag, ButtonRenderPropArg, ButtonPropsWeControl>,
ref: Ref<HTMLButtonElement> ref: Ref<HTMLButtonElement>
) { ) {
let [state, dispatch] = useMenuContext([Menu.name, Button.name].join('.')) let [state, dispatch] = useMenuContext('Menu.Button')
let buttonRef = useSyncRefs(state.buttonRef, ref) let buttonRef = useSyncRefs(state.buttonRef, ref)
let id = `headlessui-menu-button-${useId()}` let id = `headlessui-menu-button-${useId()}`
@@ -307,9 +308,10 @@ let Button = forwardRefWithAs(function Button<TTag extends ElementType = typeof
[dispatch, d, state, props.disabled] [dispatch, d, state, props.disabled]
) )
let slot = useMemo<ButtonRenderPropArg>(() => ({ open: state.menuState === MenuStates.Open }), [ let slot = useMemo<ButtonRenderPropArg>(
state, () => ({ open: state.menuState === MenuStates.Open }),
]) [state]
)
let passthroughProps = props let passthroughProps = props
let propsWeControl = { let propsWeControl = {
ref: buttonRef, ref: buttonRef,
@@ -352,7 +354,7 @@ let Items = forwardRefWithAs(function Items<TTag extends ElementType = typeof DE
PropsForFeatures<typeof ItemsRenderFeatures>, PropsForFeatures<typeof ItemsRenderFeatures>,
ref: Ref<HTMLDivElement> ref: Ref<HTMLDivElement>
) { ) {
let [state, dispatch] = useMenuContext([Menu.name, Items.name].join('.')) let [state, dispatch] = useMenuContext('Menu.Items')
let itemsRef = useSyncRefs(state.itemsRef, ref) let itemsRef = useSyncRefs(state.itemsRef, ref)
let id = `headlessui-menu-items-${useId()}` let id = `headlessui-menu-items-${useId()}`
@@ -471,9 +473,10 @@ let Items = forwardRefWithAs(function Items<TTag extends ElementType = typeof DE
} }
}, []) }, [])
let slot = useMemo<ItemsRenderPropArg>(() => ({ open: state.menuState === MenuStates.Open }), [ let slot = useMemo<ItemsRenderPropArg>(
state, () => ({ open: state.menuState === MenuStates.Open }),
]) [state]
)
let propsWeControl = { let propsWeControl = {
'aria-activedescendant': 'aria-activedescendant':
state.activeItemIndex === null ? undefined : state.items[state.activeItemIndex]?.id, state.activeItemIndex === null ? undefined : state.items[state.activeItemIndex]?.id,
@@ -522,7 +525,7 @@ function Item<TTag extends ElementType = typeof DEFAULT_ITEM_TAG>(
} }
) { ) {
let { disabled = false, onClick, ...passthroughProps } = props let { disabled = false, onClick, ...passthroughProps } = props
let [state, dispatch] = useMenuContext([Menu.name, Item.name].join('.')) let [state, dispatch] = useMenuContext('Menu.Item')
let id = `headlessui-menu-item-${useId()}` let id = `headlessui-menu-item-${useId()}`
let active = state.activeItemIndex !== null ? state.items[state.activeItemIndex].id === id : false let active = state.activeItemIndex !== null ? state.items[state.activeItemIndex].id === id : false
@@ -23,7 +23,7 @@ jest.mock('../../hooks/use-id')
afterAll(() => jest.restoreAllMocks()) afterAll(() => jest.restoreAllMocks())
function nextFrame() { function nextFrame() {
return new Promise<void>(resolve => { return new Promise<void>((resolve) => {
requestAnimationFrame(() => { requestAnimationFrame(() => {
requestAnimationFrame(() => { requestAnimationFrame(() => {
resolve() resolve()
@@ -75,7 +75,7 @@ let reducers: {
action: Extract<Actions, { type: P }> action: Extract<Actions, { type: P }>
) => StateDefinition ) => StateDefinition
} = { } = {
[ActionTypes.TogglePopover]: state => ({ [ActionTypes.TogglePopover]: (state) => ({
...state, ...state,
popoverState: match(state.popoverState, { popoverState: match(state.popoverState, {
[PopoverStates.Open]: PopoverStates.Closed, [PopoverStates.Open]: PopoverStates.Closed,
@@ -217,7 +217,7 @@ export function Popover<TTag extends ElementType = typeof DEFAULT_POPOVER_TAG>(
) )
// Handle outside click // Handle outside click
useWindowEvent('mousedown', event => { useWindowEvent('mousedown', (event) => {
let target = event.target as HTMLElement let target = event.target as HTMLElement
if (popoverState !== PopoverStates.Open) return if (popoverState !== PopoverStates.Open) return
@@ -296,7 +296,7 @@ let Button = forwardRefWithAs(function Button<TTag extends ElementType = typeof
props: Props<TTag, ButtonRenderPropArg, ButtonPropsWeControl>, props: Props<TTag, ButtonRenderPropArg, ButtonPropsWeControl>,
ref: Ref<HTMLButtonElement> ref: Ref<HTMLButtonElement>
) { ) {
let [state, dispatch] = usePopoverContext([Popover.name, Button.name].join('.')) let [state, dispatch] = usePopoverContext('Popover.Button')
let internalButtonRef = useRef<HTMLButtonElement | null>(null) let internalButtonRef = useRef<HTMLButtonElement | null>(null)
let groupContext = usePopoverGroupContext() let groupContext = usePopoverGroupContext()
@@ -308,7 +308,7 @@ let Button = forwardRefWithAs(function Button<TTag extends ElementType = typeof
let buttonRef = useSyncRefs( let buttonRef = useSyncRefs(
internalButtonRef, internalButtonRef,
ref, ref,
isWithinPanel ? null : button => dispatch({ type: ActionTypes.SetButton, button }) isWithinPanel ? null : (button) => dispatch({ type: ActionTypes.SetButton, button })
) )
let withinPanelButtonRef = useSyncRefs(internalButtonRef, ref) let withinPanelButtonRef = useSyncRefs(internalButtonRef, ref)
@@ -517,7 +517,7 @@ let Overlay = forwardRefWithAs(function Overlay<
PropsForFeatures<typeof OverlayRenderFeatures>, PropsForFeatures<typeof OverlayRenderFeatures>,
ref: Ref<HTMLDivElement> ref: Ref<HTMLDivElement>
) { ) {
let [{ popoverState }, dispatch] = usePopoverContext([Popover.name, Overlay.name].join('.')) let [{ popoverState }, dispatch] = usePopoverContext('Popover.Overlay')
let overlayRef = useSyncRefs(ref) let overlayRef = useSyncRefs(ref)
let id = `headlessui-popover-overlay-${useId()}` let id = `headlessui-popover-overlay-${useId()}`
@@ -539,9 +539,10 @@ let Overlay = forwardRefWithAs(function Overlay<
[dispatch] [dispatch]
) )
let slot = useMemo<OverlayRenderPropArg>(() => ({ open: popoverState === PopoverStates.Open }), [ let slot = useMemo<OverlayRenderPropArg>(
popoverState, () => ({ open: popoverState === PopoverStates.Open }),
]) [popoverState]
)
let propsWeControl = { let propsWeControl = {
ref: overlayRef, ref: overlayRef,
id, id,
@@ -580,11 +581,11 @@ let Panel = forwardRefWithAs(function Panel<TTag extends ElementType = typeof DE
) { ) {
let { focus = false, ...passthroughProps } = props let { focus = false, ...passthroughProps } = props
let [state, dispatch] = usePopoverContext([Popover.name, Panel.name].join('.')) let [state, dispatch] = usePopoverContext('Popover.Panel')
let { close } = usePopoverAPIContext([Popover.name, Panel.name].join('.')) let { close } = usePopoverAPIContext('Popover.Panel')
let internalPanelRef = useRef<HTMLDivElement | null>(null) let internalPanelRef = useRef<HTMLDivElement | null>(null)
let panelRef = useSyncRefs(internalPanelRef, ref, panel => { let panelRef = useSyncRefs(internalPanelRef, ref, (panel) => {
dispatch({ type: ActionTypes.SetPanel, panel }) dispatch({ type: ActionTypes.SetPanel, panel })
}) })
@@ -639,7 +640,7 @@ let Panel = forwardRefWithAs(function Panel<TTag extends ElementType = typeof DE
}, [focus, internalPanelRef, state.popoverState]) }, [focus, internalPanelRef, state.popoverState])
// Handle Tab / Shift+Tab focus positioning // Handle Tab / Shift+Tab focus positioning
useWindowEvent('keydown', event => { useWindowEvent('keydown', (event) => {
if (state.popoverState !== PopoverStates.Open) return if (state.popoverState !== PopoverStates.Open) return
if (!internalPanelRef.current) return if (!internalPanelRef.current) return
if (event.key !== Keys.Tab) return if (event.key !== Keys.Tab) return
@@ -665,7 +666,7 @@ let Panel = forwardRefWithAs(function Panel<TTag extends ElementType = typeof DE
let nextElements = elements let nextElements = elements
.splice(buttonIdx + 1) // Elements after button .splice(buttonIdx + 1) // Elements after button
.filter(element => !internalPanelRef.current?.contains(element)) // Ignore items in panel .filter((element) => !internalPanelRef.current?.contains(element)) // Ignore items in panel
// Try to focus the next element, however it could fail if we are in a // Try to focus the next element, however it could fail if we are in a
// Portal that happens to be the very last one in the DOM. In that // Portal that happens to be the very last one in the DOM. In that
@@ -730,7 +731,7 @@ function Group<TTag extends ElementType = typeof DEFAULT_PANEL_TAG>(
let unregisterPopover = useCallback( let unregisterPopover = useCallback(
(registerbag: PopoverRegisterBag) => { (registerbag: PopoverRegisterBag) => {
setPopovers(existing => { setPopovers((existing) => {
let idx = existing.indexOf(registerbag) let idx = existing.indexOf(registerbag)
if (idx !== -1) { if (idx !== -1) {
let clone = existing.slice() let clone = existing.slice()
@@ -745,7 +746,7 @@ function Group<TTag extends ElementType = typeof DEFAULT_PANEL_TAG>(
let registerPopover = useCallback( let registerPopover = useCallback(
(registerbag: PopoverRegisterBag) => { (registerbag: PopoverRegisterBag) => {
setPopovers(existing => [...existing, registerbag]) setPopovers((existing) => [...existing, registerbag])
return () => unregisterPopover(registerbag) return () => unregisterPopover(registerbag)
}, },
[setPopovers, unregisterPopover] [setPopovers, unregisterPopover]
@@ -757,7 +758,7 @@ function Group<TTag extends ElementType = typeof DEFAULT_PANEL_TAG>(
if (groupRef.current?.contains(element)) return true if (groupRef.current?.contains(element)) return true
// Check if the focus is in one of the button or panel elements. This is important in case you are rendering inside a Portal. // Check if the focus is in one of the button or panel elements. This is important in case you are rendering inside a Portal.
return popovers.some(bag => { return popovers.some((bag) => {
return ( return (
document.getElementById(bag.buttonId)?.contains(element) || document.getElementById(bag.buttonId)?.contains(element) ||
document.getElementById(bag.panelId)?.contains(element) document.getElementById(bag.panelId)?.contains(element)
@@ -82,10 +82,10 @@ it('should cleanup the Portal root when the last Portal is unmounted', async ()
return ( return (
<main id="parent"> <main id="parent">
<button id="a" onClick={() => setRenderA(v => !v)}> <button id="a" onClick={() => setRenderA((v) => !v)}>
Toggle A Toggle A
</button> </button>
<button id="b" onClick={() => setRenderB(v => !v)}> <button id="b" onClick={() => setRenderB((v) => !v)}>
Toggle B Toggle B
</button> </button>
@@ -151,21 +151,21 @@ it('should be possible to render multiple portals at the same time', async () =>
return ( return (
<main id="parent"> <main id="parent">
<button id="a" onClick={() => setRenderA(v => !v)}> <button id="a" onClick={() => setRenderA((v) => !v)}>
Toggle A Toggle A
</button> </button>
<button id="b" onClick={() => setRenderB(v => !v)}> <button id="b" onClick={() => setRenderB((v) => !v)}>
Toggle B Toggle B
</button> </button>
<button id="c" onClick={() => setRenderC(v => !v)}> <button id="c" onClick={() => setRenderC((v) => !v)}>
Toggle C Toggle C
</button> </button>
<button <button
id="double" id="double"
onClick={() => { onClick={() => {
setRenderA(v => !v) setRenderA((v) => !v)
setRenderB(v => !v) setRenderB((v) => !v)
}} }}
> >
Toggle A & B{' '} Toggle A & B{' '}
@@ -231,10 +231,10 @@ it('should be possible to tamper with the modal root and restore correctly', asy
return ( return (
<main id="parent"> <main id="parent">
<button id="a" onClick={() => setRenderA(v => !v)}> <button id="a" onClick={() => setRenderA((v) => !v)}>
Toggle A Toggle A
</button> </button>
<button id="b" onClick={() => setRenderB(v => !v)}> <button id="b" onClick={() => setRenderB((v) => !v)}>
Toggle B Toggle B
</button> </button>
@@ -113,7 +113,7 @@ describe('Rendering', () => {
return ( return (
<> <>
<button onClick={() => setShowFirst(v => !v)}>Toggle</button> <button onClick={() => setShowFirst((v) => !v)}>Toggle</button>
<RadioGroup value={active} onChange={setActive}> <RadioGroup value={active} onChange={setActive}>
<RadioGroup.Label>Pizza Delivery</RadioGroup.Label> <RadioGroup.Label>Pizza Delivery</RadioGroup.Label>
{showFirst && <RadioGroup.Option value="pickup">Pickup</RadioGroup.Option>} {showFirst && <RadioGroup.Option value="pickup">Pickup</RadioGroup.Option>}
@@ -145,7 +145,7 @@ describe('Rendering', () => {
let [disabled, setDisabled] = useState(true) let [disabled, setDisabled] = useState(true)
return ( return (
<> <>
<button onClick={() => setDisabled(v => !v)}>Toggle</button> <button onClick={() => setDisabled((v) => !v)}>Toggle</button>
<RadioGroup value={undefined} onChange={changeFn} disabled={disabled}> <RadioGroup value={undefined} onChange={changeFn} disabled={disabled}>
<RadioGroup.Label>Pizza Delivery</RadioGroup.Label> <RadioGroup.Label>Pizza Delivery</RadioGroup.Label>
<RadioGroup.Option value="pickup">Pickup</RadioGroup.Option> <RadioGroup.Option value="pickup">Pickup</RadioGroup.Option>
@@ -208,7 +208,7 @@ describe('Rendering', () => {
let [disabled, setDisabled] = useState(true) let [disabled, setDisabled] = useState(true)
return ( return (
<> <>
<button onClick={() => setDisabled(v => !v)}>Toggle</button> <button onClick={() => setDisabled((v) => !v)}>Toggle</button>
<RadioGroup value={undefined} onChange={changeFn}> <RadioGroup value={undefined} onChange={changeFn}>
<RadioGroup.Label>Pizza Delivery</RadioGroup.Label> <RadioGroup.Label>Pizza Delivery</RadioGroup.Label>
<RadioGroup.Option value="pickup">Pickup</RadioGroup.Option> <RadioGroup.Option value="pickup">Pickup</RadioGroup.Option>
@@ -745,7 +745,7 @@ describe('Keyboard interactions', () => {
<button>Before</button> <button>Before</button>
<RadioGroup <RadioGroup
value={value} value={value}
onChange={v => { onChange={(v) => {
setValue(v) setValue(v)
changeFn(v) changeFn(v)
}} }}
@@ -815,7 +815,7 @@ describe('Mouse interactions', () => {
<button>Before</button> <button>Before</button>
<RadioGroup <RadioGroup
value={value} value={value}
onChange={v => { onChange={(v) => {
setValue(v) setValue(v)
changeFn(v) changeFn(v)
}} }}
@@ -61,7 +61,7 @@ let reducers: {
}, },
[ActionTypes.UnregisterOption](state, action) { [ActionTypes.UnregisterOption](state, action) {
let options = state.options.slice() let options = state.options.slice()
let idx = state.options.findIndex(radio => radio.id === action.id) let idx = state.options.findIndex((radio) => radio.id === action.id)
if (idx === -1) return state if (idx === -1) return state
options.splice(idx, 1) options.splice(idx, 1)
return { ...state, options } return { ...state, options }
@@ -123,23 +123,23 @@ export function RadioGroup<
let firstOption = useMemo( let firstOption = useMemo(
() => () =>
options.find(option => { options.find((option) => {
if (option.propsRef.current.disabled) return false if (option.propsRef.current.disabled) return false
return true return true
}), }),
[options] [options]
) )
let containsCheckedOption = useMemo( let containsCheckedOption = useMemo(
() => options.some(option => option.propsRef.current.value === value), () => options.some((option) => option.propsRef.current.value === value),
[options, value] [options, value]
) )
let triggerChange = useCallback( let triggerChange = useCallback(
nextValue => { (nextValue) => {
if (disabled) return false if (disabled) return false
if (nextValue === value) return false if (nextValue === value) return false
let nextOption = options.find(option => option.propsRef.current.value === nextValue)?.propsRef let nextOption = options.find((option) => option.propsRef.current.value === nextValue)
.current ?.propsRef.current
if (nextOption?.disabled) return false if (nextOption?.disabled) return false
onChange(nextValue) onChange(nextValue)
@@ -166,8 +166,8 @@ export function RadioGroup<
if (!container) return if (!container) return
let all = options let all = options
.filter(option => option.propsRef.current.disabled === false) .filter((option) => option.propsRef.current.disabled === false)
.map(radio => radio.element.current) as HTMLElement[] .map((radio) => radio.element.current) as HTMLElement[]
switch (event.key) { switch (event.key) {
case Keys.ArrowLeft: case Keys.ArrowLeft:
@@ -180,7 +180,7 @@ export function RadioGroup<
if (result === FocusResult.Success) { if (result === FocusResult.Success) {
let activeOption = options.find( let activeOption = options.find(
option => option.element.current === document.activeElement (option) => option.element.current === document.activeElement
) )
if (activeOption) triggerChange(activeOption.propsRef.current.value) if (activeOption) triggerChange(activeOption.propsRef.current.value)
} }
@@ -197,7 +197,7 @@ export function RadioGroup<
if (result === FocusResult.Success) { if (result === FocusResult.Success) {
let activeOption = options.find( let activeOption = options.find(
option => option.element.current === document.activeElement (option) => option.element.current === document.activeElement
) )
if (activeOption) triggerChange(activeOption.propsRef.current.value) if (activeOption) triggerChange(activeOption.propsRef.current.value)
} }
@@ -210,7 +210,7 @@ export function RadioGroup<
event.stopPropagation() event.stopPropagation()
let activeOption = options.find( let activeOption = options.find(
option => option.element.current === document.activeElement (option) => option.element.current === document.activeElement
) )
if (activeOption) triggerChange(activeOption.propsRef.current.value) if (activeOption) triggerChange(activeOption.propsRef.current.value)
} }
@@ -322,14 +322,12 @@ function Option<
firstOption, firstOption,
containsCheckedOption, containsCheckedOption,
value: radioGroupValue, value: radioGroupValue,
} = useRadioGroupContext([RadioGroup.name, Option.name].join('.')) } = useRadioGroupContext('RadioGroup.Option')
useIsoMorphicEffect(() => registerOption({ id, element: optionRef, propsRef }), [ useIsoMorphicEffect(
id, () => registerOption({ id, element: optionRef, propsRef }),
registerOption, [id, registerOption, optionRef, props]
optionRef, )
props,
])
let handleClick = useCallback(() => { let handleClick = useCallback(() => {
if (!change(value)) return if (!change(value)) return
@@ -214,7 +214,7 @@ describe('Keyboard interactions', () => {
return ( return (
<Switch <Switch
checked={state} checked={state}
onChange={value => { onChange={(value) => {
setState(value) setState(value)
handleChange(value) handleChange(value)
}} }}
@@ -297,7 +297,7 @@ describe('Mouse interactions', () => {
return ( return (
<Switch <Switch
checked={state} checked={state}
onChange={value => { onChange={(value) => {
setState(value) setState(value)
handleChange(value) handleChange(value)
}} }}
@@ -331,7 +331,7 @@ describe('Mouse interactions', () => {
<Switch.Group> <Switch.Group>
<Switch <Switch
checked={state} checked={state}
onChange={value => { onChange={(value) => {
setState(value) setState(value)
handleChange(value) handleChange(value)
}} }}
@@ -373,7 +373,7 @@ describe('Mouse interactions', () => {
<Switch.Group> <Switch.Group>
<Switch <Switch
checked={state} checked={state}
onChange={value => { onChange={(value) => {
setState(value) setState(value)
handleChange(value) handleChange(value)
}} }}
@@ -81,7 +81,7 @@ describe('Rendering', () => {
return ( return (
<> <>
<button onClick={() => setHide(v => !v)}>toggle</button> <button onClick={() => setHide((v) => !v)}>toggle</button>
<Tab.Group> <Tab.Group>
<Tab.List> <Tab.List>
<Tab>Tab 1</Tab> <Tab>Tab 1</Tab>
@@ -118,7 +118,7 @@ describe('Rendering', () => {
it('should expose the `selectedIndex` on the `Tab.Group` component', async () => { it('should expose the `selectedIndex` on the `Tab.Group` component', async () => {
render( render(
<Tab.Group> <Tab.Group>
{data => ( {(data) => (
<> <>
<pre id="exposed">{JSON.stringify(data)}</pre> <pre id="exposed">{JSON.stringify(data)}</pre>
@@ -153,7 +153,7 @@ describe('Rendering', () => {
render( render(
<Tab.Group> <Tab.Group>
<Tab.List> <Tab.List>
{data => ( {(data) => (
<> <>
<pre id="exposed">{JSON.stringify(data)}</pre> <pre id="exposed">{JSON.stringify(data)}</pre>
<Tab>Tab 1</Tab> <Tab>Tab 1</Tab>
@@ -192,7 +192,7 @@ describe('Rendering', () => {
</Tab.List> </Tab.List>
<Tab.Panels> <Tab.Panels>
{data => ( {(data) => (
<> <>
<pre id="exposed">{JSON.stringify(data)}</pre> <pre id="exposed">{JSON.stringify(data)}</pre>
<Tab.Panel>Content 1</Tab.Panel> <Tab.Panel>Content 1</Tab.Panel>
@@ -220,7 +220,7 @@ describe('Rendering', () => {
<Tab.Group> <Tab.Group>
<Tab.List> <Tab.List>
<Tab> <Tab>
{data => ( {(data) => (
<> <>
<pre data-tab={0}>{JSON.stringify(data)}</pre> <pre data-tab={0}>{JSON.stringify(data)}</pre>
<span>Tab 1</span> <span>Tab 1</span>
@@ -228,7 +228,7 @@ describe('Rendering', () => {
)} )}
</Tab> </Tab>
<Tab> <Tab>
{data => ( {(data) => (
<> <>
<pre data-tab={1}>{JSON.stringify(data)}</pre> <pre data-tab={1}>{JSON.stringify(data)}</pre>
<span>Tab 2</span> <span>Tab 2</span>
@@ -236,7 +236,7 @@ describe('Rendering', () => {
)} )}
</Tab> </Tab>
<Tab> <Tab>
{data => ( {(data) => (
<> <>
<pre data-tab={2}>{JSON.stringify(data)}</pre> <pre data-tab={2}>{JSON.stringify(data)}</pre>
<span>Tab 3</span> <span>Tab 3</span>
@@ -287,7 +287,7 @@ describe('Rendering', () => {
<Tab.Panels> <Tab.Panels>
<Tab.Panel unmount={false}> <Tab.Panel unmount={false}>
{data => ( {(data) => (
<> <>
<pre data-panel={0}>{JSON.stringify(data)}</pre> <pre data-panel={0}>{JSON.stringify(data)}</pre>
<span>Content 1</span> <span>Content 1</span>
@@ -295,7 +295,7 @@ describe('Rendering', () => {
)} )}
</Tab.Panel> </Tab.Panel>
<Tab.Panel unmount={false}> <Tab.Panel unmount={false}>
{data => ( {(data) => (
<> <>
<pre data-panel={1}>{JSON.stringify(data)}</pre> <pre data-panel={1}>{JSON.stringify(data)}</pre>
<span>Content 2</span> <span>Content 2</span>
@@ -303,7 +303,7 @@ describe('Rendering', () => {
)} )}
</Tab.Panel> </Tab.Panel>
<Tab.Panel unmount={false}> <Tab.Panel unmount={false}>
{data => ( {(data) => (
<> <>
<pre data-panel={2}>{JSON.stringify(data)}</pre> <pre data-panel={2}>{JSON.stringify(data)}</pre>
<span>Content 3</span> <span>Content 3</span>
@@ -514,7 +514,7 @@ describe('Rendering', () => {
<> <>
<Tab.Group <Tab.Group
selectedIndex={selectedIndex} selectedIndex={selectedIndex}
onChange={value => { onChange={(value) => {
setSelectedIndex(value) setSelectedIndex(value)
handleChange(value) handleChange(value)
}} }}
@@ -533,7 +533,7 @@ describe('Rendering', () => {
</Tab.Group> </Tab.Group>
<button>after</button> <button>after</button>
<button onClick={() => setSelectedIndex(prev => prev + 1)}>setSelectedIndex</button> <button onClick={() => setSelectedIndex((prev) => prev + 1)}>setSelectedIndex</button>
</> </>
) )
} }
@@ -83,14 +83,14 @@ let reducers: {
return { ...state, tabs: [...state.tabs, action.tab] } return { ...state, tabs: [...state.tabs, action.tab] }
}, },
[ActionTypes.UnregisterTab](state, action) { [ActionTypes.UnregisterTab](state, action) {
return { ...state, tabs: state.tabs.filter(tab => tab !== action.tab) } return { ...state, tabs: state.tabs.filter((tab) => tab !== action.tab) }
}, },
[ActionTypes.RegisterPanel](state, action) { [ActionTypes.RegisterPanel](state, action) {
if (state.panels.includes(action.panel)) return state if (state.panels.includes(action.panel)) return state
return { ...state, panels: [...state.panels, action.panel] } return { ...state, panels: [...state.panels, action.panel] }
}, },
[ActionTypes.UnregisterPanel](state, action) { [ActionTypes.UnregisterPanel](state, action) {
return { ...state, panels: state.panels.filter(panel => panel !== action.panel) } return { ...state, panels: state.panels.filter((panel) => panel !== action.panel) }
}, },
[ActionTypes.ForceRerender](state) { [ActionTypes.ForceRerender](state) {
return { ...state } return { ...state }
@@ -171,8 +171,8 @@ function Tabs<TTag extends ElementType = typeof DEFAULT_TABS_TAG>(
if (state.tabs.length <= 0) return if (state.tabs.length <= 0) return
if (selectedIndex === null && state.selectedIndex !== null) return if (selectedIndex === null && state.selectedIndex !== null) return
let tabs = state.tabs.map(tab => tab.current).filter(Boolean) as HTMLElement[] let tabs = state.tabs.map((tab) => tab.current).filter(Boolean) as HTMLElement[]
let focusableTabs = tabs.filter(tab => !tab.hasAttribute('disabled')) let focusableTabs = tabs.filter((tab) => !tab.hasAttribute('disabled'))
let indexToSet = selectedIndex ?? defaultIndex let indexToSet = selectedIndex ?? defaultIndex
@@ -194,7 +194,7 @@ function Tabs<TTag extends ElementType = typeof DEFAULT_TABS_TAG>(
let before = tabs.slice(0, indexToSet) let before = tabs.slice(0, indexToSet)
let after = tabs.slice(indexToSet) let after = tabs.slice(indexToSet)
let next = [...after, ...before].find(tab => focusableTabs.includes(tab)) let next = [...after, ...before].find((tab) => focusableTabs.includes(tab))
if (!next) return if (!next) return
dispatch({ type: ActionTypes.SetSelectedIndex, index: tabs.indexOf(next) }) dispatch({ type: ActionTypes.SetSelectedIndex, index: tabs.indexOf(next) })
@@ -245,7 +245,7 @@ type ListPropsWeControl = 'role' | 'aria-orientation'
function List<TTag extends ElementType = typeof DEFAULT_LIST_TAG>( function List<TTag extends ElementType = typeof DEFAULT_LIST_TAG>(
props: Props<TTag, ListRenderPropArg, ListPropsWeControl> & {} props: Props<TTag, ListRenderPropArg, ListPropsWeControl> & {}
) { ) {
let [{ selectedIndex, orientation }] = useTabsContext([Tab.name, List.name].join('.')) let [{ selectedIndex, orientation }] = useTabsContext('Tab.List')
let slot = { selectedIndex } let slot = { selectedIndex }
let propsWeControl = { let propsWeControl = {
@@ -275,13 +275,11 @@ export function Tab<TTag extends ElementType = typeof DEFAULT_TAB_TAG>(
) { ) {
let id = `headlessui-tabs-tab-${useId()}` let id = `headlessui-tabs-tab-${useId()}`
let [ let [{ selectedIndex, tabs, panels, orientation, activation }, { dispatch, change }] =
{ selectedIndex, tabs, panels, orientation, activation }, useTabsContext(Tab.name)
{ dispatch, change },
] = useTabsContext(Tab.name)
let internalTabRef = useRef<HTMLElement>(null) let internalTabRef = useRef<HTMLElement>(null)
let tabRef = useSyncRefs(internalTabRef, element => { let tabRef = useSyncRefs(internalTabRef, (element) => {
if (!element) return if (!element) return
dispatch({ type: ActionTypes.ForceRerender }) dispatch({ type: ActionTypes.ForceRerender })
}) })
@@ -296,7 +294,7 @@ export function Tab<TTag extends ElementType = typeof DEFAULT_TAB_TAG>(
let handleKeyDown = useCallback( let handleKeyDown = useCallback(
(event: ReactKeyboardEvent<HTMLElement>) => { (event: ReactKeyboardEvent<HTMLElement>) => {
let list = tabs.map(tab => tab.current).filter(Boolean) as HTMLElement[] let list = tabs.map((tab) => tab.current).filter(Boolean) as HTMLElement[]
if (event.key === Keys.Space || event.key === Keys.Enter) { if (event.key === Keys.Space || event.key === Keys.Enter) {
event.preventDefault() event.preventDefault()
@@ -380,7 +378,7 @@ interface PanelsRenderPropArg {
function Panels<TTag extends ElementType = typeof DEFAULT_PANELS_TAG>( function Panels<TTag extends ElementType = typeof DEFAULT_PANELS_TAG>(
props: Props<TTag, PanelsRenderPropArg> props: Props<TTag, PanelsRenderPropArg>
) { ) {
let [{ selectedIndex }] = useTabsContext([Tab.name, Panels.name].join('.')) let [{ selectedIndex }] = useTabsContext('Tab.Panels')
let slot = useMemo(() => ({ selectedIndex }), [selectedIndex]) let slot = useMemo(() => ({ selectedIndex }), [selectedIndex])
@@ -405,13 +403,11 @@ function Panel<TTag extends ElementType = typeof DEFAULT_PANEL_TAG>(
props: Props<TTag, PanelRenderPropArg, PanelPropsWeControl> & props: Props<TTag, PanelRenderPropArg, PanelPropsWeControl> &
PropsForFeatures<typeof PanelRenderFeatures> PropsForFeatures<typeof PanelRenderFeatures>
) { ) {
let [{ selectedIndex, tabs, panels }, { dispatch }] = useTabsContext( let [{ selectedIndex, tabs, panels }, { dispatch }] = useTabsContext('Tab.Panel')
[Tab.name, Panel.name].join('.')
)
let id = `headlessui-tabs-panel-${useId()}` let id = `headlessui-tabs-panel-${useId()}`
let internalPanelRef = useRef<HTMLElement>(null) let internalPanelRef = useRef<HTMLElement>(null)
let panelRef = useSyncRefs(internalPanelRef, element => { let panelRef = useSyncRefs(internalPanelRef, (element) => {
if (!element) return if (!element) return
dispatch({ type: ActionTypes.ForceRerender }) dispatch({ type: ActionTypes.ForceRerender })
}) })
@@ -26,8 +26,6 @@ it(
expect(() => { expect(() => {
render( render(
// @ts-expect-error Disabling TS because it does require us to use a show prop. But non
// TypeScript projects won't benefit from this.
<Transition> <Transition>
<div className="hello">Children</div> <div className="hello">Children</div>
</Transition> </Transition>
@@ -445,7 +443,7 @@ describe('Transitions', () => {
<span>Hello!</span> <span>Hello!</span>
</Transition> </Transition>
<button data-testid="toggle" onClick={() => setShow(v => !v)}> <button data-testid="toggle" onClick={() => setShow((v) => !v)}>
Toggle Toggle
</button> </button>
</> </>
@@ -488,14 +486,15 @@ describe('Transitions', () => {
return ( return (
<> <>
<style>{`.enter { transition-duration: ${enterDuration / <style>{`.enter { transition-duration: ${
1000}s; } .from { opacity: 0%; } .to { opacity: 100%; }`}</style> enterDuration / 1000
}s; } .from { opacity: 0%; } .to { opacity: 100%; }`}</style>
<Transition show={show} enter="enter" enterFrom="from" enterTo="to"> <Transition show={show} enter="enter" enterFrom="from" enterTo="to">
<span>Hello!</span> <span>Hello!</span>
</Transition> </Transition>
<button data-testid="toggle" onClick={() => setShow(v => !v)}> <button data-testid="toggle" onClick={() => setShow((v) => !v)}>
Toggle Toggle
</button> </button>
</> </>
@@ -538,14 +537,15 @@ describe('Transitions', () => {
return ( return (
<> <>
<style>{`.enter { transition-duration: ${enterDuration / <style>{`.enter { transition-duration: ${
1000}s; } .from { opacity: 0%; } .to { opacity: 100%; }`}</style> enterDuration / 1000
}s; } .from { opacity: 0%; } .to { opacity: 100%; }`}</style>
<Transition show={show} unmount={false} enter="enter" enterFrom="from" enterTo="to"> <Transition show={show} unmount={false} enter="enter" enterFrom="from" enterTo="to">
<span>Hello!</span> <span>Hello!</span>
</Transition> </Transition>
<button data-testid="toggle" onClick={() => setShow(v => !v)}> <button data-testid="toggle" onClick={() => setShow((v) => !v)}>
Toggle Toggle
</button> </button>
</> </>
@@ -591,7 +591,7 @@ describe('Transitions', () => {
<span>Hello!</span> <span>Hello!</span>
</Transition> </Transition>
<button data-testid="toggle" onClick={() => setShow(v => !v)}> <button data-testid="toggle" onClick={() => setShow((v) => !v)}>
Toggle Toggle
</button> </button>
</> </>
@@ -642,7 +642,7 @@ describe('Transitions', () => {
<span>Hello!</span> <span>Hello!</span>
</Transition> </Transition>
<button data-testid="toggle" onClick={() => setShow(v => !v)}> <button data-testid="toggle" onClick={() => setShow((v) => !v)}>
Toggle Toggle
</button> </button>
</> </>
@@ -696,7 +696,7 @@ describe('Transitions', () => {
<span>Hello!</span> <span>Hello!</span>
</Transition> </Transition>
<button data-testid="toggle" onClick={() => setShow(v => !v)}> <button data-testid="toggle" onClick={() => setShow((v) => !v)}>
Toggle Toggle
</button> </button>
</> </>
@@ -757,7 +757,7 @@ describe('Transitions', () => {
<span>Hello!</span> <span>Hello!</span>
</Transition> </Transition>
<button data-testid="toggle" onClick={() => setShow(v => !v)}> <button data-testid="toggle" onClick={() => setShow((v) => !v)}>
Toggle Toggle
</button> </button>
</> </>
@@ -843,7 +843,7 @@ describe('Transitions', () => {
<span>Hello!</span> <span>Hello!</span>
</Transition> </Transition>
<button data-testid="toggle" onClick={() => setShow(v => !v)}> <button data-testid="toggle" onClick={() => setShow((v) => !v)}>
Toggle Toggle
</button> </button>
</> </>
@@ -943,7 +943,7 @@ describe('Transitions', () => {
</Transition.Child> </Transition.Child>
</Transition> </Transition>
<button data-testid="toggle" onClick={() => setShow(v => !v)}> <button data-testid="toggle" onClick={() => setShow((v) => !v)}>
Toggle Toggle
</button> </button>
</> </>
@@ -1027,7 +1027,7 @@ describe('Transitions', () => {
</Transition.Child> </Transition.Child>
</Transition> </Transition>
<button data-testid="toggle" onClick={() => setShow(v => !v)}> <button data-testid="toggle" onClick={() => setShow((v) => !v)}>
Toggle Toggle
</button> </button>
</> </>
@@ -1138,7 +1138,7 @@ describe('Events', () => {
<span>Hello!</span> <span>Hello!</span>
</Transition> </Transition>
<button data-testid="toggle" onClick={() => setShow(v => !v)}> <button data-testid="toggle" onClick={() => setShow((v) => !v)}>
Toggle Toggle
</button> </button>
</> </>
@@ -28,9 +28,10 @@ import { useServerHandoffComplete } from '../../hooks/use-server-handoff-complet
type ID = ReturnType<typeof useId> type ID = ReturnType<typeof useId>
function useSplitClasses(classes: string = '') { function useSplitClasses(classes: string = '') {
return useMemo(() => classes.split(' ').filter(className => className.trim().length > 1), [ return useMemo(
classes, () => classes.split(' ').filter((className) => className.trim().length > 1),
]) [classes]
)
} }
interface TransitionContextValues { interface TransitionContextValues {
@@ -287,23 +288,37 @@ function TransitionChild<TTag extends ElementType = typeof DEFAULT_TRANSITION_CH
if (!show) events.current.beforeLeave() if (!show) events.current.beforeLeave()
return show return show
? transition(node, enterClasses, enterFromClasses, enterToClasses, enteredClasses, reason => { ? transition(
isTransitioning.current = false node,
if (reason === Reason.Finished) events.current.afterEnter() enterClasses,
}) enterFromClasses,
: transition(node, leaveClasses, leaveFromClasses, leaveToClasses, enteredClasses, reason => { enterToClasses,
isTransitioning.current = false enteredClasses,
(reason) => {
if (reason !== Reason.Finished) return isTransitioning.current = false
if (reason === Reason.Finished) events.current.afterEnter()
// When we don't have children anymore we can safely unregister from the parent and hide
// ourselves.
if (!hasChildren(nesting)) {
setState(TreeStates.Hidden)
unregister(id)
events.current.afterLeave()
} }
}) )
: transition(
node,
leaveClasses,
leaveFromClasses,
leaveToClasses,
enteredClasses,
(reason) => {
isTransitioning.current = false
if (reason !== Reason.Finished) return
// When we don't have children anymore we can safely unregister from the parent and hide
// ourselves.
if (!hasChildren(nesting)) {
setState(TreeStates.Hidden)
unregister(id)
events.current.afterLeave()
}
}
)
}, [ }, [
events, events,
id, id,
@@ -359,7 +374,7 @@ export function Transition<TTag extends ElementType = typeof DEFAULT_TRANSITION_
}) })
} }
if (![true, false].includes((show as unknown) as boolean)) { if (![true, false].includes(show as unknown as boolean)) {
throw new Error('A <Transition /> is used but it is missing a `show={true | false}` prop.') throw new Error('A <Transition /> is used but it is missing a `show={true | false}` prop.')
} }
@@ -17,7 +17,7 @@ it('should be possible to transition', async () => {
d.add( d.add(
reportChanges( reportChanges(
() => document.body.innerHTML, () => document.body.innerHTML,
content => { (content) => {
snapshots.push({ snapshots.push({
content, content,
recordedAt: process.hrtime.bigint(), recordedAt: process.hrtime.bigint(),
@@ -26,11 +26,11 @@ it('should be possible to transition', async () => {
) )
) )
await new Promise(resolve => { await new Promise((resolve) => {
transition(element, ['enter'], ['enterFrom'], ['enterTo'], ['entered'], resolve) transition(element, ['enter'], ['enterFrom'], ['enterTo'], ['entered'], resolve)
}) })
await new Promise(resolve => d.nextFrame(resolve)) await new Promise((resolve) => d.nextFrame(resolve))
// Initial render: // Initial render:
expect(snapshots[0].content).toEqual('<div></div>') expect(snapshots[0].content).toEqual('<div></div>')
@@ -61,7 +61,7 @@ it('should wait the correct amount of time to finish a transition', async () =>
d.add( d.add(
reportChanges( reportChanges(
() => document.body.innerHTML, () => document.body.innerHTML,
content => { (content) => {
snapshots.push({ snapshots.push({
content, content,
recordedAt: process.hrtime.bigint(), recordedAt: process.hrtime.bigint(),
@@ -70,11 +70,11 @@ it('should wait the correct amount of time to finish a transition', async () =>
) )
) )
let reason = await new Promise(resolve => { let reason = await new Promise((resolve) => {
transition(element, ['enter'], ['enterFrom'], ['enterTo'], ['entered'], resolve) transition(element, ['enter'], ['enterFrom'], ['enterTo'], ['entered'], resolve)
}) })
await new Promise(resolve => d.nextFrame(resolve)) await new Promise((resolve) => d.nextFrame(resolve))
expect(reason).toBe(Reason.Finished) expect(reason).toBe(Reason.Finished)
// Initial render: // Initial render:
@@ -118,7 +118,7 @@ it('should keep the delay time into account', async () => {
d.add( d.add(
reportChanges( reportChanges(
() => document.body.innerHTML, () => document.body.innerHTML,
content => { (content) => {
snapshots.push({ snapshots.push({
content, content,
recordedAt: process.hrtime.bigint(), recordedAt: process.hrtime.bigint(),
@@ -127,11 +127,11 @@ it('should keep the delay time into account', async () => {
) )
) )
let reason = await new Promise(resolve => { let reason = await new Promise((resolve) => {
transition(element, ['enter'], ['enterFrom'], ['enterTo'], ['entered'], resolve) transition(element, ['enter'], ['enterFrom'], ['enterTo'], ['entered'], resolve)
}) })
await new Promise(resolve => d.nextFrame(resolve)) await new Promise((resolve) => d.nextFrame(resolve))
expect(reason).toBe(Reason.Finished) expect(reason).toBe(Reason.Finished)
let estimatedDuration = Number( let estimatedDuration = Number(
@@ -161,7 +161,7 @@ it('should be possible to cancel a transition at any time', async () => {
d.add( d.add(
reportChanges( reportChanges(
() => document.body.innerHTML, () => document.body.innerHTML,
content => { (content) => {
let recordedAt = process.hrtime.bigint() let recordedAt = process.hrtime.bigint()
let total = snapshots.length let total = snapshots.length
@@ -178,16 +178,16 @@ it('should be possible to cancel a transition at any time', async () => {
expect.assertions(2) expect.assertions(2)
// Setup the transition // Setup the transition
let cancel = transition(element, ['enter'], ['enterFrom'], ['enterTo'], ['entered'], reason => { let cancel = transition(element, ['enter'], ['enterFrom'], ['enterTo'], ['entered'], (reason) => {
expect(reason).toBe(Reason.Cancelled) expect(reason).toBe(Reason.Cancelled)
}) })
// Wait for a bit // Wait for a bit
await new Promise(resolve => setTimeout(resolve, 20)) await new Promise((resolve) => setTimeout(resolve, 20))
// Cancel the transition // Cancel the transition
cancel() cancel()
await new Promise(resolve => d.nextFrame(resolve)) await new Promise((resolve) => d.nextFrame(resolve))
expect(snapshots.map(snapshot => snapshot.content).join('\n')).not.toContain('enterTo') expect(snapshots.map((snapshot) => snapshot.content).join('\n')).not.toContain('enterTo')
}) })
@@ -22,13 +22,13 @@ function waitForTransition(node: HTMLElement, done: (reason: Reason) => void) {
// Safari returns a comma separated list of values, so let's sort them and take the highest value. // Safari returns a comma separated list of values, so let's sort them and take the highest value.
let { transitionDuration, transitionDelay } = getComputedStyle(node) let { transitionDuration, transitionDelay } = getComputedStyle(node)
let [durationMs, delaysMs] = [transitionDuration, transitionDelay].map(value => { let [durationMs, delaysMs] = [transitionDuration, transitionDelay].map((value) => {
let [resolvedValue = 0] = value let [resolvedValue = 0] = value
.split(',') .split(',')
// Remove falsy we can't work with // Remove falsy we can't work with
.filter(Boolean) .filter(Boolean)
// Values are returned as `0.3s` or `75ms` // Values are returned as `0.3s` or `75ms`
.map(v => (v.includes('ms') ? parseFloat(v) : parseFloat(v) * 1000)) .map((v) => (v.includes('ms') ? parseFloat(v) : parseFloat(v) * 1000))
.sort((a, z) => z - a) .sort((a, z) => z - a)
return resolvedValue return resolvedValue
@@ -74,7 +74,7 @@ export function transition(
addClasses(node, ...to) addClasses(node, ...to)
d.add( d.add(
waitForTransition(node, reason => { waitForTransition(node, (reason) => {
removeClasses(node, ...to, ...base) removeClasses(node, ...to, ...base)
addClasses(node, ...entered) addClasses(node, ...entered)
return _done(reason) return _done(reason)
@@ -3,10 +3,10 @@ import { useState, useCallback } from 'react'
export function useFlags(initialFlags = 0) { export function useFlags(initialFlags = 0) {
let [flags, setFlags] = useState(initialFlags) let [flags, setFlags] = useState(initialFlags)
let addFlag = useCallback((flag: number) => setFlags(flags => flags | flag), [setFlags]) let addFlag = useCallback((flag: number) => setFlags((flags) => flags | flag), [setFlags])
let hasFlag = useCallback((flag: number) => Boolean(flags & flag), [flags]) let hasFlag = useCallback((flag: number) => Boolean(flags & flag), [flags])
let removeFlag = useCallback((flag: number) => setFlags(flags => flags & ~flag), [setFlags]) let removeFlag = useCallback((flag: number) => setFlags((flags) => flags & ~flag), [setFlags])
let toggleFlag = useCallback((flag: number) => setFlags(flags => flags ^ flag), [setFlags]) let toggleFlag = useCallback((flag: number) => setFlags((flags) => flags ^ flag), [setFlags])
return { addFlag, hasFlag, removeFlag, toggleFlag } return { addFlag, hasFlag, removeFlag, toggleFlag }
} }
@@ -97,7 +97,7 @@ export function useFocusTrap(
}, [container, initialFocus, featuresInitialFocus]) }, [container, initialFocus, featuresInitialFocus])
// Handle `Tab` & `Shift+Tab` keyboard events // Handle `Tab` & `Shift+Tab` keyboard events
useWindowEvent('keydown', event => { useWindowEvent('keydown', (event) => {
if (!(features & Features.TabLock)) return if (!(features & Features.TabLock)) return
if (!container.current) return if (!container.current) return
@@ -118,7 +118,7 @@ export function useFocusTrap(
// Prevent programmatically escaping the container // Prevent programmatically escaping the container
useWindowEvent( useWindowEvent(
'focus', 'focus',
event => { (event) => {
if (!(features & Features.FocusLock)) return if (!(features & Features.FocusLock)) return
let allContainers = new Set(containers?.current) let allContainers = new Set(containers?.current)
@@ -17,7 +17,7 @@ it('should be possible to inert other elements', async () => {
return ( return (
<div ref={ref} id="main"> <div ref={ref} id="main">
<button onClick={() => setEnabled(v => !v)}>toggle</button> <button onClick={() => setEnabled((v) => !v)}>toggle</button>
</div> </div>
) )
} }
@@ -61,7 +61,7 @@ it('should restore inert elements, when all useInertOthers calls are disabled',
return ( return (
<div ref={ref} id={id}> <div ref={ref} id={id}>
<button onClick={() => setEnabled(v => !v)}>{toggle}</button> <button onClick={() => setEnabled((v) => !v)}>{toggle}</button>
</div> </div>
) )
} }
@@ -136,7 +136,7 @@ it('should restore inert elements, when all useInertOthers calls are disabled (i
return ( return (
<div id={`parent-${id}`}> <div id={`parent-${id}`}>
<div ref={ref} id={id}> <div ref={ref} id={id}>
<button onClick={() => setEnabled(v => !v)}>{toggle}</button> <button onClick={() => setEnabled((v) => !v)}>{toggle}</button>
</div> </div>
</div> </div>
) )
@@ -221,7 +221,7 @@ it('should handle inert others correctly when 2 useInertOthers are used in a sha
return ( return (
<div ref={ref} id={id}> <div ref={ref} id={id}>
<button onClick={() => setEnabled(v => !v)}>{toggle}</button> <button onClick={() => setEnabled((v) => !v)}>{toggle}</button>
</div> </div>
) )
} }
@@ -42,7 +42,7 @@ export function useInertOthers<TElement extends HTMLElement>(
} }
// Collect direct children of the body // Collect direct children of the body
document.querySelectorAll('body > *').forEach(child => { document.querySelectorAll('body > *').forEach((child) => {
if (!(child instanceof HTMLElement)) return // Skip non-HTMLElements if (!(child instanceof HTMLElement)) return // Skip non-HTMLElements
// Skip the interactables, and the parents of the interactables // Skip the interactables, and the parents of the interactables
@@ -71,7 +71,7 @@ export function useInertOthers<TElement extends HTMLElement>(
// will become inert as well. // will become inert as well.
if (interactables.size > 0) { if (interactables.size > 0) {
// Collect direct children of the body // Collect direct children of the body
document.querySelectorAll('body > *').forEach(child => { document.querySelectorAll('body > *').forEach((child) => {
if (!(child instanceof HTMLElement)) return // Skip non-HTMLElements if (!(child instanceof HTMLElement)) return // Skip non-HTMLElements
// Skip already inert parents // Skip already inert parents
@@ -35,6 +35,7 @@ export function useTreeWalker({
let walk = walkRef.current let walk = walkRef.current
let acceptNode = Object.assign((node: HTMLElement) => accept(node), { acceptNode: accept }) let acceptNode = Object.assign((node: HTMLElement) => accept(node), { acceptNode: accept })
// @ts-expect-error This `false` is a simple small fix for older browsers
let walker = document.createTreeWalker(container, NodeFilter.SHOW_ELEMENT, acceptNode, false) let walker = document.createTreeWalker(container, NodeFilter.SHOW_ELEMENT, acceptNode, false)
while (walker.nextNode()) walk(walker.currentNode as HTMLElement) while (walker.nextNode()) walk(walker.currentNode as HTMLElement)
@@ -1256,7 +1256,7 @@ export function assertLabelValue(element: HTMLElement | null, value: string) {
if (element.hasAttribute('aria-labelledby')) { if (element.hasAttribute('aria-labelledby')) {
let ids = element.getAttribute('aria-labelledby')!.split(' ') let ids = element.getAttribute('aria-labelledby')!.split(' ')
expect(ids.map(id => document.getElementById(id)?.textContent).join(' ')).toEqual(value) expect(ids.map((id) => document.getElementById(id)?.textContent).join(' ')).toEqual(value)
return return
} }
@@ -1612,7 +1612,7 @@ export function assertTabs(
expect(list).toHaveAttribute('aria-orientation', orientation) expect(list).toHaveAttribute('aria-orientation', orientation)
let activeTab = Array.from(list.querySelectorAll('[id^="headlessui-tabs-tab-"]'))[active] let activeTab = Array.from(list.querySelectorAll('[id^="headlessui-tabs-tab-"]'))[active]
let activePanel = panels.find(panel => panel.id === activeTab.getAttribute('aria-controls')) let activePanel = panels.find((panel) => panel.id === activeTab.getAttribute('aria-controls'))
for (let tab of tabs) { for (let tab of tabs) {
expect(tab).toHaveAttribute('id') expect(tab).toHaveAttribute('id')
@@ -17,7 +17,7 @@ function redentSnapshot(input: string) {
return input return input
.split('\n') .split('\n')
.map(line => .map((line) =>
line.trim() === '---' ? line : line.replace(replacer, (_, sign, rest) => `${sign} ${rest}`) line.trim() === '---' ? line : line.replace(replacer, (_, sign, rest) => `${sign} ${rest}`)
) )
.join('\n') .join('\n')
@@ -69,13 +69,13 @@ export async function executeTimeline(
.reduce((total, current) => total + current, 0) .reduce((total, current) => total + current, 0)
// Changes happen in the next frame // Changes happen in the next frame
await new Promise(resolve => d.nextFrame(resolve)) await new Promise((resolve) => d.nextFrame(resolve))
// We wait for the amount of the duration // We wait for the amount of the duration
await new Promise(resolve => d.setTimeout(resolve, totalDuration)) await new Promise((resolve) => d.setTimeout(resolve, totalDuration))
// We wait an additional next frame so that we know that we are done // We wait an additional next frame so that we know that we are done
await new Promise(resolve => d.nextFrame(resolve)) await new Promise((resolve) => d.nextFrame(resolve))
}, Promise.resolve()) }, Promise.resolve())
if (snapshots.length <= 0) { if (snapshots.length <= 0) {
@@ -127,7 +127,7 @@ export async function executeTimeline(
.replace(/Snapshot Diff:\n/g, '') .replace(/Snapshot Diff:\n/g, '')
) )
.split('\n') .split('\n')
.map(line => ` ${line}`) .map((line) => ` ${line}`)
.join('\n')}` .join('\n')}`
}) })
.filter(Boolean) .filter(Boolean)
@@ -175,7 +175,7 @@ describe('Keyboard', () => {
await type([key(input)]) await type([key(input)])
let expected = result.map(e => event(e)) let expected = result.map((e) => event(e))
expect(fired.length).toEqual(result.length) expect(fired.length).toEqual(result.length)
@@ -36,7 +36,7 @@ export function shift(event: Partial<KeyboardEvent>) {
} }
export function word(input: string): Partial<KeyboardEvent>[] { export function word(input: string): Partial<KeyboardEvent>[] {
let result = input.split('').map(key => ({ key })) let result = input.split('').map((key) => ({ key }))
d.enqueue(() => { d.enqueue(() => {
let element = document.activeElement let element = document.activeElement
@@ -152,7 +152,7 @@ export async function type(events: Partial<KeyboardEvent>[], element = document.
let actions = order[event.key!] ?? order[Default as any] let actions = order[event.key!] ?? order[Default as any]
for (let action of actions) { for (let action of actions) {
let checks = action.name.split('And') let checks = action.name.split('And')
if (checks.some(check => skip.has(check))) continue if (checks.some((check) => skip.has(check))) continue
let result = action(element, { let result = action(element, {
type: action.name, type: action.name,
@@ -344,8 +344,8 @@ let focusableSelector = [
? // TODO: Remove this once JSDOM fixes the issue where an element that is ? // TODO: Remove this once JSDOM fixes the issue where an element that is
// "hidden" can be the document.activeElement, because this is not possible // "hidden" can be the document.activeElement, because this is not possible
// in real browsers. // in real browsers.
selector => `${selector}:not([tabindex='-1']):not([style*='display: none'])` (selector) => `${selector}:not([tabindex='-1']):not([style*='display: none'])`
: selector => `${selector}:not([tabindex='-1'])` : (selector) => `${selector}:not([tabindex='-1'])`
) )
.join(',') .join(',')
@@ -5,10 +5,10 @@ type FunctionPropertyNames<T> = {
export function suppressConsoleLogs<T extends unknown[]>( export function suppressConsoleLogs<T extends unknown[]>(
cb: (...args: T) => unknown, cb: (...args: T) => unknown,
type: FunctionPropertyNames<typeof global.console> = 'error' type: FunctionPropertyNames<typeof globalThis.console> = 'error'
) { ) {
return (...args: T) => { return (...args: T) => {
let spy = jest.spyOn(global.console, type).mockImplementation(jest.fn()) let spy = jest.spyOn(globalThis.console, type).mockImplementation(jest.fn())
return new Promise<unknown>((resolve, reject) => { return new Promise<unknown>((resolve, reject) => {
Promise.resolve(cb(...args)).then(resolve, reject) Promise.resolve(cb(...args)).then(resolve, reject)
@@ -40,7 +40,7 @@ export function calculateActiveIndex<TItem>(
let nextActiveIndex = (() => { let nextActiveIndex = (() => {
switch (action.focus) { switch (action.focus) {
case Focus.First: case Focus.First:
return items.findIndex(item => !resolvers.resolveDisabled(item)) return items.findIndex((item) => !resolvers.resolveDisabled(item))
case Focus.Previous: { case Focus.Previous: {
let idx = items let idx = items
@@ -64,13 +64,13 @@ export function calculateActiveIndex<TItem>(
let idx = items let idx = items
.slice() .slice()
.reverse() .reverse()
.findIndex(item => !resolvers.resolveDisabled(item)) .findIndex((item) => !resolvers.resolveDisabled(item))
if (idx === -1) return idx if (idx === -1) return idx
return items.length - 1 - idx return items.length - 1 - idx
} }
case Focus.Specific: case Focus.Specific:
return items.findIndex(item => resolvers.resolveId(item) === action.id) return items.findIndex((item) => resolvers.resolveId(item) === action.id)
case Focus.Nothing: case Focus.Nothing:
return null return null
@@ -18,8 +18,8 @@ let focusableSelector = [
? // TODO: Remove this once JSDOM fixes the issue where an element that is ? // TODO: Remove this once JSDOM fixes the issue where an element that is
// "hidden" can be the document.activeElement, because this is not possible // "hidden" can be the document.activeElement, because this is not possible
// in real browsers. // in real browsers.
selector => `${selector}:not([tabindex='-1']):not([style*='display: none'])` (selector) => `${selector}:not([tabindex='-1']):not([style*='display: none'])`
: selector => `${selector}:not([tabindex='-1'])` : (selector) => `${selector}:not([tabindex='-1'])`
) )
.join(',') .join(',')
@@ -12,7 +12,7 @@ export function match<TValue extends string | number = string, TReturnValue = un
`Tried to handle "${value}" but there is no handler defined. Only defined handlers are: ${Object.keys( `Tried to handle "${value}" but there is no handler defined. Only defined handlers are: ${Object.keys(
lookup lookup
) )
.map(key => `"${key}"`) .map((key) => `"${key}"`)
.join(', ')}.` .join(', ')}.`
) )
if (Error.captureStackTrace) Error.captureStackTrace(error, match) if (Error.captureStackTrace) Error.captureStackTrace(error, match)
@@ -45,7 +45,7 @@ describe('Default functionality', () => {
testRender( testRender(
<Dummy> <Dummy>
{data => { {(data) => {
expect(data).toBe(slot) expect(data).toBe(slot)
return <span>Contents</span> return <span>Contents</span>
@@ -102,10 +102,12 @@ function _render<TTag extends ElementType, TSlot>(
tag: ElementType, tag: ElementType,
name: string name: string
) { ) {
let { as: Component = tag, children, refName = 'ref', ...passThroughProps } = omit(props, [ let {
'unmount', as: Component = tag,
'static', children,
]) refName = 'ref',
...passThroughProps
} = omit(props, ['unmount', 'static'])
// This allows us to use `<HeadlessUIComponent as={MyComponent} refName="innerRef" />` // This allows us to use `<HeadlessUIComponent as={MyComponent} refName="innerRef" />`
let refRelatedProps = props.ref !== undefined ? { [refName]: props.ref } : {} let refRelatedProps = props.ref !== undefined ? { [refName]: props.ref } : {}
@@ -132,7 +134,7 @@ function _render<TTag extends ElementType, TSlot>(
`The current component <${name} /> is rendering a "Fragment".`, `The current component <${name} /> is rendering a "Fragment".`,
`However we need to passthrough the following props:`, `However we need to passthrough the following props:`,
Object.keys(passThroughProps) Object.keys(passThroughProps)
.map(line => ` - ${line}`) .map((line) => ` - ${line}`)
.join('\n'), .join('\n'),
'', '',
'You can apply a few solutions:', 'You can apply a few solutions:',
@@ -140,7 +142,7 @@ function _render<TTag extends ElementType, TSlot>(
'Add an `as="..."` prop, to ensure that we render an actual element instead of a "Fragment".', 'Add an `as="..."` prop, to ensure that we render an actual element instead of a "Fragment".',
'Render a single element as the child so that we can forward the props onto that element.', 'Render a single element as the child so that we can forward the props onto that element.',
] ]
.map(line => ` - ${line}`) .map((line) => ` - ${line}`)
.join('\n'), .join('\n'),
].join('\n') ].join('\n')
) )
@@ -211,7 +213,7 @@ function mergeEventFunctions(
export function forwardRefWithAs<T extends { name: string; displayName?: string }>( export function forwardRefWithAs<T extends { name: string; displayName?: string }>(
component: T component: T
): T & { displayName: string } { ): T & { displayName: string } {
return Object.assign(forwardRef((component as unknown) as any) as any, { return Object.assign(forwardRef(component as unknown as any) as any, {
displayName: component.displayName ?? component.name, displayName: component.displayName ?? component.name,
}) })
} }
+2 -3
View File
@@ -21,13 +21,12 @@
}, },
"jsx": "preserve", "jsx": "preserve",
"esModuleInterop": true, "esModuleInterop": true,
"target": "es5", "target": "ESNext",
"allowJs": true, "allowJs": true,
"skipLibCheck": true, "skipLibCheck": true,
"forceConsistentCasingInFileNames": true, "forceConsistentCasingInFileNames": true,
"noEmit": true,
"resolveJsonModule": true, "resolveJsonModule": true,
"isolatedModules": true "isolatedModules": true
}, },
"exclude": ["node_modules"] "exclude": ["node_modules", "**/*.test.tsx?"]
} }
@@ -1,7 +0,0 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"jsx": "react"
}
}
-10
View File
@@ -1,10 +0,0 @@
module.exports = {
rollup(config, opts) {
if (opts.format === 'esm') {
config = { ...config, preserveModules: true }
config.output = { ...config.output, dir: 'dist/', entryFileNames: '[name].esm.js' }
delete config.output.file
}
return config
},
}
+9
View File
@@ -0,0 +1,9 @@
export {}
declare global {
namespace jest {
interface Matchers<R> {
toBeWithinRenderFrame(actual: number): R
}
}
}
-6
View File
@@ -1,6 +0,0 @@
module.exports = {
rules: {
'react-hooks/rules-of-hooks': 'off',
'react-hooks/exhaustive-deps': 'off',
},
}
+7
View File
@@ -0,0 +1,7 @@
'use strict'
if (process.env.NODE_ENV === 'production') {
module.exports = require('./headlessui.prod.cjs.js')
} else {
module.exports = require('./headlessui.dev.cjs.js')
}
+16 -5
View File
@@ -4,12 +4,21 @@
"description": "A set of completely unstyled, fully accessible UI components for Vue 3, designed to integrate beautifully with Tailwind CSS.", "description": "A set of completely unstyled, fully accessible UI components for Vue 3, designed to integrate beautifully with Tailwind CSS.",
"main": "dist/index.js", "main": "dist/index.js",
"typings": "dist/index.d.ts", "typings": "dist/index.d.ts",
"module": "dist/index.esm.js", "module": "dist/headlessui.esm.js",
"license": "MIT", "license": "MIT",
"files": [ "files": [
"README.md", "README.md",
"dist" "dist"
], ],
"exports": {
".": {
"import": {
"default": "./dist/headlessui.esm.js"
},
"require": "./dist/index.js",
"types": "./dist/index.d.ts"
}
},
"sideEffects": false, "sideEffects": false,
"engines": { "engines": {
"node": ">=10" "node": ">=10"
@@ -27,14 +36,16 @@
"build": "../../scripts/build.sh", "build": "../../scripts/build.sh",
"watch": "../../scripts/watch.sh", "watch": "../../scripts/watch.sh",
"test": "../../scripts/test.sh", "test": "../../scripts/test.sh",
"lint": "../../scripts/lint.sh" "lint": "../../scripts/lint.sh",
"playground": "yarn workspace playground-vue dev",
"clean": "rimraf ./dist"
}, },
"peerDependencies": { "peerDependencies": {
"vue": "^3.0.0" "vue": "^3.0.0"
}, },
"devDependencies": { "devDependencies": {
"@testing-library/vue": "^5.1.0", "@testing-library/vue": "^5.8.2",
"@vue/test-utils": "^2.0.0-beta.7", "@vue/test-utils": "^2.0.0-rc.18",
"vue": "3.0.7" "vue": "^3.2.27"
} }
} }
@@ -1 +0,0 @@
module.exports = require('../../postcss.config.js')
@@ -66,7 +66,7 @@ beforeAll(() => {
afterAll(() => jest.restoreAllMocks()) afterAll(() => jest.restoreAllMocks())
function nextFrame() { function nextFrame() {
return new Promise<void>(resolve => { return new Promise<void>((resolve) => {
requestAnimationFrame(() => { requestAnimationFrame(() => {
requestAnimationFrame(() => { requestAnimationFrame(() => {
resolve() resolve()
@@ -95,9 +95,9 @@ function renderTemplate(input: string | Partial<DefineComponent>) {
return render( return render(
defineComponent( defineComponent(
(Object.assign({}, input, { Object.assign({}, input, {
components: { ...defaultComponents, ...input.components }, components: { ...defaultComponents, ...input.components },
}) as unknown) as DefineComponent }) as Parameters<typeof defineComponent>[0]
) )
) )
} }
@@ -491,9 +491,7 @@ describe('Rendering', () => {
template: html` template: html`
<Combobox v-model="value"> <Combobox v-model="value">
<ComboboxInput /> <ComboboxInput />
<ComboboxButton type="submit"> <ComboboxButton type="submit"> Trigger </ComboboxButton>
Trigger
</ComboboxButton>
</Combobox> </Combobox>
`, `,
setup: () => ({ value: ref(null) }), setup: () => ({ value: ref(null) }),
@@ -506,16 +504,14 @@ describe('Rendering', () => {
'should set the `type` to "button" when using the `as` prop which resolves to a "button"', 'should set the `type` to "button" when using the `as` prop which resolves to a "button"',
suppressConsoleLogs(async () => { suppressConsoleLogs(async () => {
let CustomButton = defineComponent({ let CustomButton = defineComponent({
setup: props => () => h('button', { ...props }), setup: (props) => () => h('button', { ...props }),
}) })
renderTemplate({ renderTemplate({
template: html` template: html`
<Combobox v-model="value"> <Combobox v-model="value">
<ComboboxInput /> <ComboboxInput />
<ComboboxButton :as="CustomButton"> <ComboboxButton :as="CustomButton"> Trigger </ComboboxButton>
Trigger
</ComboboxButton>
</Combobox> </Combobox>
`, `,
setup: () => ({ setup: () => ({
@@ -535,9 +531,7 @@ describe('Rendering', () => {
template: html` template: html`
<Combobox v-model="value"> <Combobox v-model="value">
<ComboboxInput /> <ComboboxInput />
<ComboboxButton as="div"> <ComboboxButton as="div"> Trigger </ComboboxButton>
Trigger
</ComboboxButton>
</Combobox> </Combobox>
`, `,
setup: () => ({ value: ref(null) }), setup: () => ({ value: ref(null) }),
@@ -550,16 +544,14 @@ describe('Rendering', () => {
'should not set the `type` to "button" when using the `as` prop which resolves to a "div"', 'should not set the `type` to "button" when using the `as` prop which resolves to a "div"',
suppressConsoleLogs(async () => { suppressConsoleLogs(async () => {
let CustomButton = defineComponent({ let CustomButton = defineComponent({
setup: props => () => h('div', props), setup: (props) => () => h('div', props),
}) })
renderTemplate({ renderTemplate({
template: html` template: html`
<Combobox v-model="value"> <Combobox v-model="value">
<ComboboxInput /> <ComboboxInput />
<ComboboxButton :as="CustomButton"> <ComboboxButton :as="CustomButton"> Trigger </ComboboxButton>
Trigger
</ComboboxButton>
</Combobox> </Combobox>
`, `,
setup: () => ({ setup: () => ({
@@ -765,15 +757,9 @@ describe('Rendering composition', () => {
<ComboboxInput /> <ComboboxInput />
<ComboboxButton>Trigger</ComboboxButton> <ComboboxButton>Trigger</ComboboxButton>
<ComboboxOptions> <ComboboxOptions>
<ComboboxOption as="button" value="a"> <ComboboxOption as="button" value="a"> Option A </ComboboxOption>
Option A <ComboboxOption as="button" value="b"> Option B </ComboboxOption>
</ComboboxOption> <ComboboxOption as="button" value="c"> Option C </ComboboxOption>
<ComboboxOption as="button" value="b">
Option B
</ComboboxOption>
<ComboboxOption as="button" value="c">
Option C
</ComboboxOption>
</ComboboxOptions> </ComboboxOptions>
</Combobox> </Combobox>
`, `,
@@ -790,7 +776,7 @@ describe('Rendering composition', () => {
await click(getComboboxButton()) await click(getComboboxButton())
// Verify options are buttons now // Verify options are buttons now
getComboboxOptions().forEach(option => assertComboboxOption(option, { tag: 'button' })) getComboboxOptions().forEach((option) => assertComboboxOption(option, { tag: 'button' }))
}) })
) )
}) })
@@ -823,9 +809,7 @@ describe('Composition', () => {
<ComboboxInput /> <ComboboxInput />
<ComboboxButton>Trigger</ComboboxButton> <ComboboxButton>Trigger</ComboboxButton>
<OpenClosedWrite :open="true"> <OpenClosedWrite :open="true">
<ComboboxOptions v-slot="data"> <ComboboxOptions v-slot="data"> {{JSON.stringify(data)}} </ComboboxOptions>
{{JSON.stringify(data)}}
</ComboboxOptions>
</OpenClosedWrite> </OpenClosedWrite>
</Combobox> </Combobox>
`, `,
@@ -854,9 +838,7 @@ describe('Composition', () => {
<ComboboxInput /> <ComboboxInput />
<ComboboxButton>Trigger</ComboboxButton> <ComboboxButton>Trigger</ComboboxButton>
<OpenClosedWrite :open="false"> <OpenClosedWrite :open="false">
<ComboboxOptions v-slot="data"> <ComboboxOptions v-slot="data"> {{JSON.stringify(data)}} </ComboboxOptions>
{{JSON.stringify(data)}}
</ComboboxOptions>
</OpenClosedWrite> </OpenClosedWrite>
</Combobox> </Combobox>
`, `,
@@ -966,7 +948,7 @@ describe('Keyboard interactions', () => {
// Verify we have combobox options // Verify we have combobox options
let options = getComboboxOptions() let options = getComboboxOptions()
expect(options).toHaveLength(3) expect(options).toHaveLength(3)
options.forEach(option => assertComboboxOption(option, { selected: false })) options.forEach((option) => assertComboboxOption(option, { selected: false }))
assertNoActiveComboboxOption() assertNoActiveComboboxOption()
assertNoSelectedComboboxOption() assertNoSelectedComboboxOption()
@@ -1274,7 +1256,7 @@ describe('Keyboard interactions', () => {
// Verify we have combobox options // Verify we have combobox options
let options = getComboboxOptions() let options = getComboboxOptions()
expect(options).toHaveLength(3) expect(options).toHaveLength(3)
options.forEach(option => assertComboboxOption(option)) options.forEach((option) => assertComboboxOption(option))
assertNoActiveComboboxOption() assertNoActiveComboboxOption()
}) })
) )
@@ -1408,15 +1390,9 @@ describe('Keyboard interactions', () => {
<ComboboxInput /> <ComboboxInput />
<ComboboxButton>Trigger</ComboboxButton> <ComboboxButton>Trigger</ComboboxButton>
<ComboboxOptions> <ComboboxOptions>
<ComboboxOption disabled value="a"> <ComboboxOption disabled value="a"> Option A </ComboboxOption>
Option A <ComboboxOption disabled value="b"> Option B </ComboboxOption>
</ComboboxOption> <ComboboxOption disabled value="c"> Option C </ComboboxOption>
<ComboboxOption disabled value="b">
Option B
</ComboboxOption>
<ComboboxOption disabled value="c">
Option C
</ComboboxOption>
</ComboboxOptions> </ComboboxOptions>
</Combobox> </Combobox>
`, `,
@@ -1533,7 +1509,7 @@ describe('Keyboard interactions', () => {
// Verify we have combobox options // Verify we have combobox options
let options = getComboboxOptions() let options = getComboboxOptions()
expect(options).toHaveLength(3) expect(options).toHaveLength(3)
options.forEach(option => assertComboboxOption(option)) options.forEach((option) => assertComboboxOption(option))
// Verify that the first combobox option is active // Verify that the first combobox option is active
assertNoActiveComboboxOption() assertNoActiveComboboxOption()
@@ -1701,7 +1677,7 @@ describe('Keyboard interactions', () => {
// Verify we have combobox options // Verify we have combobox options
let options = getComboboxOptions() let options = getComboboxOptions()
expect(options).toHaveLength(3) expect(options).toHaveLength(3)
options.forEach(option => assertComboboxOption(option)) options.forEach((option) => assertComboboxOption(option))
// Verify that the first combobox option is active // Verify that the first combobox option is active
assertNoActiveComboboxOption() assertNoActiveComboboxOption()
@@ -1869,7 +1845,7 @@ describe('Keyboard interactions', () => {
// Verify we have combobox options // Verify we have combobox options
let options = getComboboxOptions() let options = getComboboxOptions()
expect(options).toHaveLength(3) expect(options).toHaveLength(3)
options.forEach(option => assertComboboxOption(option)) options.forEach((option) => assertComboboxOption(option))
// ! ALERT: The LAST option should now be active // ! ALERT: The LAST option should now be active
assertActiveComboboxOption(options[2]) assertActiveComboboxOption(options[2])
@@ -2002,12 +1978,8 @@ describe('Keyboard interactions', () => {
<ComboboxButton>Trigger</ComboboxButton> <ComboboxButton>Trigger</ComboboxButton>
<ComboboxOptions> <ComboboxOptions>
<ComboboxOption value="a">Option A</ComboboxOption> <ComboboxOption value="a">Option A</ComboboxOption>
<ComboboxOption disabled value="b"> <ComboboxOption disabled value="b"> Option B </ComboboxOption>
Option B <ComboboxOption disabled value="c"> Option C </ComboboxOption>
</ComboboxOption>
<ComboboxOption disabled value="c">
Option C
</ComboboxOption>
</ComboboxOptions> </ComboboxOptions>
</Combobox> </Combobox>
`, `,
@@ -2029,7 +2001,7 @@ describe('Keyboard interactions', () => {
// Verify we have combobox options // Verify we have combobox options
let options = getComboboxOptions() let options = getComboboxOptions()
expect(options).toHaveLength(3) expect(options).toHaveLength(3)
options.forEach(option => assertComboboxOption(option)) options.forEach((option) => assertComboboxOption(option))
assertActiveComboboxOption(options[0]) assertActiveComboboxOption(options[0])
}) })
) )
@@ -2079,7 +2051,7 @@ describe('Keyboard interactions', () => {
// Verify we have combobox options // Verify we have combobox options
let options = getComboboxOptions() let options = getComboboxOptions()
expect(options).toHaveLength(3) expect(options).toHaveLength(3)
options.forEach(option => assertComboboxOption(option)) options.forEach((option) => assertComboboxOption(option))
// ! ALERT: The LAST option should now be active // ! ALERT: The LAST option should now be active
assertActiveComboboxOption(options[2]) assertActiveComboboxOption(options[2])
@@ -2213,12 +2185,8 @@ describe('Keyboard interactions', () => {
<ComboboxButton>Trigger</ComboboxButton> <ComboboxButton>Trigger</ComboboxButton>
<ComboboxOptions> <ComboboxOptions>
<ComboboxOption value="a">Option A</ComboboxOption> <ComboboxOption value="a">Option A</ComboboxOption>
<ComboboxOption disabled value="b"> <ComboboxOption disabled value="b"> Option B </ComboboxOption>
Option B <ComboboxOption disabled value="c"> Option C </ComboboxOption>
</ComboboxOption>
<ComboboxOption disabled value="c">
Option C
</ComboboxOption>
</ComboboxOptions> </ComboboxOptions>
</Combobox> </Combobox>
`, `,
@@ -2240,7 +2208,7 @@ describe('Keyboard interactions', () => {
// Verify we have combobox options // Verify we have combobox options
let options = getComboboxOptions() let options = getComboboxOptions()
expect(options).toHaveLength(3) expect(options).toHaveLength(3)
options.forEach(option => assertComboboxOption(option)) options.forEach((option) => assertComboboxOption(option))
assertActiveComboboxOption(options[0]) assertActiveComboboxOption(options[0])
}) })
) )
@@ -2496,7 +2464,7 @@ describe('Keyboard interactions', () => {
// Verify we have combobox options // Verify we have combobox options
let options = getComboboxOptions() let options = getComboboxOptions()
expect(options).toHaveLength(3) expect(options).toHaveLength(3)
options.forEach(option => assertComboboxOption(option)) options.forEach((option) => assertComboboxOption(option))
// Verify that the first combobox option is active // Verify that the first combobox option is active
assertNoActiveComboboxOption() assertNoActiveComboboxOption()
@@ -2649,7 +2617,7 @@ describe('Keyboard interactions', () => {
// Verify we have combobox options // Verify we have combobox options
let options = getComboboxOptions() let options = getComboboxOptions()
expect(options).toHaveLength(3) expect(options).toHaveLength(3)
options.forEach(option => assertComboboxOption(option)) options.forEach((option) => assertComboboxOption(option))
assertNoActiveComboboxOption() assertNoActiveComboboxOption()
// We should be able to go down once // We should be able to go down once
@@ -2679,9 +2647,7 @@ describe('Keyboard interactions', () => {
<ComboboxInput /> <ComboboxInput />
<ComboboxButton>Trigger</ComboboxButton> <ComboboxButton>Trigger</ComboboxButton>
<ComboboxOptions> <ComboboxOptions>
<ComboboxOption disabled value="a"> <ComboboxOption disabled value="a"> Option A </ComboboxOption>
Option A
</ComboboxOption>
<ComboboxOption value="b">Option B</ComboboxOption> <ComboboxOption value="b">Option B</ComboboxOption>
<ComboboxOption value="c">Option C</ComboboxOption> <ComboboxOption value="c">Option C</ComboboxOption>
</ComboboxOptions> </ComboboxOptions>
@@ -2702,7 +2668,7 @@ describe('Keyboard interactions', () => {
// Verify we have combobox options // Verify we have combobox options
let options = getComboboxOptions() let options = getComboboxOptions()
expect(options).toHaveLength(3) expect(options).toHaveLength(3)
options.forEach(option => assertComboboxOption(option)) options.forEach((option) => assertComboboxOption(option))
assertNoActiveComboboxOption() assertNoActiveComboboxOption()
// We should be able to go down once // We should be able to go down once
@@ -2720,12 +2686,8 @@ describe('Keyboard interactions', () => {
<ComboboxInput /> <ComboboxInput />
<ComboboxButton>Trigger</ComboboxButton> <ComboboxButton>Trigger</ComboboxButton>
<ComboboxOptions> <ComboboxOptions>
<ComboboxOption disabled value="a"> <ComboboxOption disabled value="a"> Option A </ComboboxOption>
Option A <ComboboxOption disabled value="b"> Option B </ComboboxOption>
</ComboboxOption>
<ComboboxOption disabled value="b">
Option B
</ComboboxOption>
<ComboboxOption value="c">Option C</ComboboxOption> <ComboboxOption value="c">Option C</ComboboxOption>
</ComboboxOptions> </ComboboxOptions>
</Combobox> </Combobox>
@@ -2745,7 +2707,7 @@ describe('Keyboard interactions', () => {
// Verify we have combobox options // Verify we have combobox options
let options = getComboboxOptions() let options = getComboboxOptions()
expect(options).toHaveLength(3) expect(options).toHaveLength(3)
options.forEach(option => assertComboboxOption(option)) options.forEach((option) => assertComboboxOption(option))
assertNoActiveComboboxOption() assertNoActiveComboboxOption()
// Open combobox // Open combobox
@@ -2799,7 +2761,7 @@ describe('Keyboard interactions', () => {
// Verify we have combobox options // Verify we have combobox options
let options = getComboboxOptions() let options = getComboboxOptions()
expect(options).toHaveLength(3) expect(options).toHaveLength(3)
options.forEach(option => assertComboboxOption(option)) options.forEach((option) => assertComboboxOption(option))
// Verify that the first combobox option is active // Verify that the first combobox option is active
assertNoActiveComboboxOption() assertNoActiveComboboxOption()
@@ -2968,7 +2930,7 @@ describe('Keyboard interactions', () => {
// Verify we have combobox options // Verify we have combobox options
let options = getComboboxOptions() let options = getComboboxOptions()
expect(options).toHaveLength(3) expect(options).toHaveLength(3)
options.forEach(option => assertComboboxOption(option)) options.forEach((option) => assertComboboxOption(option))
assertNoActiveComboboxOption() assertNoActiveComboboxOption()
// We should be able to go down once // We should be able to go down once
@@ -2998,9 +2960,7 @@ describe('Keyboard interactions', () => {
<ComboboxInput /> <ComboboxInput />
<ComboboxButton>Trigger</ComboboxButton> <ComboboxButton>Trigger</ComboboxButton>
<ComboboxOptions> <ComboboxOptions>
<ComboboxOption disabled value="a"> <ComboboxOption disabled value="a"> Option A </ComboboxOption>
Option A
</ComboboxOption>
<ComboboxOption value="b">Option B</ComboboxOption> <ComboboxOption value="b">Option B</ComboboxOption>
<ComboboxOption value="c">Option C</ComboboxOption> <ComboboxOption value="c">Option C</ComboboxOption>
</ComboboxOptions> </ComboboxOptions>
@@ -3021,7 +2981,7 @@ describe('Keyboard interactions', () => {
// Verify we have combobox options // Verify we have combobox options
let options = getComboboxOptions() let options = getComboboxOptions()
expect(options).toHaveLength(3) expect(options).toHaveLength(3)
options.forEach(option => assertComboboxOption(option)) options.forEach((option) => assertComboboxOption(option))
assertNoActiveComboboxOption() assertNoActiveComboboxOption()
// We should be able to go down once // We should be able to go down once
@@ -3039,12 +2999,8 @@ describe('Keyboard interactions', () => {
<ComboboxInput /> <ComboboxInput />
<ComboboxButton>Trigger</ComboboxButton> <ComboboxButton>Trigger</ComboboxButton>
<ComboboxOptions> <ComboboxOptions>
<ComboboxOption disabled value="a"> <ComboboxOption disabled value="a"> Option A </ComboboxOption>
Option A <ComboboxOption disabled value="b"> Option B </ComboboxOption>
</ComboboxOption>
<ComboboxOption disabled value="b">
Option B
</ComboboxOption>
<ComboboxOption value="c">Option C</ComboboxOption> <ComboboxOption value="c">Option C</ComboboxOption>
</ComboboxOptions> </ComboboxOptions>
</Combobox> </Combobox>
@@ -3064,7 +3020,7 @@ describe('Keyboard interactions', () => {
// Verify we have combobox options // Verify we have combobox options
let options = getComboboxOptions() let options = getComboboxOptions()
expect(options).toHaveLength(3) expect(options).toHaveLength(3)
options.forEach(option => assertComboboxOption(option)) options.forEach((option) => assertComboboxOption(option))
assertNoActiveComboboxOption() assertNoActiveComboboxOption()
// Open combobox // Open combobox
@@ -3117,7 +3073,7 @@ describe('Keyboard interactions', () => {
// Verify we have combobox options // Verify we have combobox options
let options = getComboboxOptions() let options = getComboboxOptions()
expect(options).toHaveLength(3) expect(options).toHaveLength(3)
options.forEach(option => assertComboboxOption(option)) options.forEach((option) => assertComboboxOption(option))
// ! ALERT: The LAST option should now be active // ! ALERT: The LAST option should now be active
assertActiveComboboxOption(options[2]) assertActiveComboboxOption(options[2])
@@ -3250,12 +3206,8 @@ describe('Keyboard interactions', () => {
<ComboboxButton>Trigger</ComboboxButton> <ComboboxButton>Trigger</ComboboxButton>
<ComboboxOptions> <ComboboxOptions>
<ComboboxOption value="a">Option A</ComboboxOption> <ComboboxOption value="a">Option A</ComboboxOption>
<ComboboxOption disabled value="b"> <ComboboxOption disabled value="b"> Option B </ComboboxOption>
Option B <ComboboxOption disabled value="c"> Option C </ComboboxOption>
</ComboboxOption>
<ComboboxOption disabled value="c">
Option C
</ComboboxOption>
</ComboboxOptions> </ComboboxOptions>
</Combobox> </Combobox>
`, `,
@@ -3277,7 +3229,7 @@ describe('Keyboard interactions', () => {
// Verify we have combobox options // Verify we have combobox options
let options = getComboboxOptions() let options = getComboboxOptions()
expect(options).toHaveLength(3) expect(options).toHaveLength(3)
options.forEach(option => assertComboboxOption(option)) options.forEach((option) => assertComboboxOption(option))
assertActiveComboboxOption(options[0]) assertActiveComboboxOption(options[0])
}) })
) )
@@ -3291,12 +3243,8 @@ describe('Keyboard interactions', () => {
<ComboboxInput /> <ComboboxInput />
<ComboboxButton>Trigger</ComboboxButton> <ComboboxButton>Trigger</ComboboxButton>
<ComboboxOptions> <ComboboxOptions>
<ComboboxOption disabled value="a"> <ComboboxOption disabled value="a"> Option A </ComboboxOption>
Option A <ComboboxOption disabled value="b"> Option B </ComboboxOption>
</ComboboxOption>
<ComboboxOption disabled value="b">
Option B
</ComboboxOption>
<ComboboxOption value="c">Option C</ComboboxOption> <ComboboxOption value="c">Option C</ComboboxOption>
</ComboboxOptions> </ComboboxOptions>
</Combobox> </Combobox>
@@ -3316,7 +3264,7 @@ describe('Keyboard interactions', () => {
// Verify we have combobox options // Verify we have combobox options
let options = getComboboxOptions() let options = getComboboxOptions()
expect(options).toHaveLength(3) expect(options).toHaveLength(3)
options.forEach(option => assertComboboxOption(option)) options.forEach((option) => assertComboboxOption(option))
assertNoActiveComboboxOption() assertNoActiveComboboxOption()
// Going up or down should select the single available option // Going up or down should select the single available option
@@ -3374,7 +3322,7 @@ describe('Keyboard interactions', () => {
// Verify we have combobox options // Verify we have combobox options
let options = getComboboxOptions() let options = getComboboxOptions()
expect(options).toHaveLength(3) expect(options).toHaveLength(3)
options.forEach(option => assertComboboxOption(option)) options.forEach((option) => assertComboboxOption(option))
assertActiveComboboxOption(options[2]) assertActiveComboboxOption(options[2])
// We should be able to go down once // We should be able to go down once
@@ -3436,7 +3384,7 @@ describe('Keyboard interactions', () => {
// Verify we have combobox options // Verify we have combobox options
let options = getComboboxOptions() let options = getComboboxOptions()
expect(options).toHaveLength(3) expect(options).toHaveLength(3)
options.forEach(option => assertComboboxOption(option)) options.forEach((option) => assertComboboxOption(option))
// ! ALERT: The LAST option should now be active // ! ALERT: The LAST option should now be active
assertActiveComboboxOption(options[2]) assertActiveComboboxOption(options[2])
@@ -3570,12 +3518,8 @@ describe('Keyboard interactions', () => {
<ComboboxButton>Trigger</ComboboxButton> <ComboboxButton>Trigger</ComboboxButton>
<ComboboxOptions> <ComboboxOptions>
<ComboboxOption value="a">Option A</ComboboxOption> <ComboboxOption value="a">Option A</ComboboxOption>
<ComboboxOption disabled value="b"> <ComboboxOption disabled value="b"> Option B </ComboboxOption>
Option B <ComboboxOption disabled value="c"> Option C </ComboboxOption>
</ComboboxOption>
<ComboboxOption disabled value="c">
Option C
</ComboboxOption>
</ComboboxOptions> </ComboboxOptions>
</Combobox> </Combobox>
`, `,
@@ -3597,7 +3541,7 @@ describe('Keyboard interactions', () => {
// Verify we have combobox options // Verify we have combobox options
let options = getComboboxOptions() let options = getComboboxOptions()
expect(options).toHaveLength(3) expect(options).toHaveLength(3)
options.forEach(option => assertComboboxOption(option)) options.forEach((option) => assertComboboxOption(option))
assertActiveComboboxOption(options[0]) assertActiveComboboxOption(options[0])
}) })
) )
@@ -3647,12 +3591,8 @@ describe('Keyboard interactions', () => {
<ComboboxOptions> <ComboboxOptions>
<ComboboxOption value="a">Option A</ComboboxOption> <ComboboxOption value="a">Option A</ComboboxOption>
<ComboboxOption value="b">Option B</ComboboxOption> <ComboboxOption value="b">Option B</ComboboxOption>
<ComboboxOption disabled value="c"> <ComboboxOption disabled value="c"> Option C </ComboboxOption>
Option C <ComboboxOption disabled value="d"> Option D </ComboboxOption>
</ComboboxOption>
<ComboboxOption disabled value="d">
Option D
</ComboboxOption>
</ComboboxOptions> </ComboboxOptions>
</Combobox> </Combobox>
`, `,
@@ -3683,15 +3623,9 @@ describe('Keyboard interactions', () => {
<ComboboxButton>Trigger</ComboboxButton> <ComboboxButton>Trigger</ComboboxButton>
<ComboboxOptions> <ComboboxOptions>
<ComboboxOption value="a">Option A</ComboboxOption> <ComboboxOption value="a">Option A</ComboboxOption>
<ComboboxOption disabled value="b"> <ComboboxOption disabled value="b"> Option B </ComboboxOption>
Option B <ComboboxOption disabled value="c"> Option C </ComboboxOption>
</ComboboxOption> <ComboboxOption disabled value="d"> Option D </ComboboxOption>
<ComboboxOption disabled value="c">
Option C
</ComboboxOption>
<ComboboxOption disabled value="d">
Option D
</ComboboxOption>
</ComboboxOptions> </ComboboxOptions>
</Combobox> </Combobox>
`, `,
@@ -3721,18 +3655,10 @@ describe('Keyboard interactions', () => {
<ComboboxInput /> <ComboboxInput />
<ComboboxButton>Trigger</ComboboxButton> <ComboboxButton>Trigger</ComboboxButton>
<ComboboxOptions> <ComboboxOptions>
<ComboboxOption disabled value="a"> <ComboboxOption disabled value="a"> Option A </ComboboxOption>
Option A <ComboboxOption disabled value="b"> Option B </ComboboxOption>
</ComboboxOption> <ComboboxOption disabled value="c"> Option C </ComboboxOption>
<ComboboxOption disabled value="b"> <ComboboxOption disabled value="d"> Option D </ComboboxOption>
Option B
</ComboboxOption>
<ComboboxOption disabled value="c">
Option C
</ComboboxOption>
<ComboboxOption disabled value="d">
Option D
</ComboboxOption>
</ComboboxOptions> </ComboboxOptions>
</Combobox> </Combobox>
`, `,
@@ -3797,12 +3723,8 @@ describe('Keyboard interactions', () => {
<ComboboxOptions> <ComboboxOptions>
<ComboboxOption value="a">Option A</ComboboxOption> <ComboboxOption value="a">Option A</ComboboxOption>
<ComboboxOption value="b">Option B</ComboboxOption> <ComboboxOption value="b">Option B</ComboboxOption>
<ComboboxOption disabled value="c"> <ComboboxOption disabled value="c"> Option C </ComboboxOption>
Option C <ComboboxOption disabled value="d"> Option D </ComboboxOption>
</ComboboxOption>
<ComboboxOption disabled value="d">
Option D
</ComboboxOption>
</ComboboxOptions> </ComboboxOptions>
</Combobox> </Combobox>
`, `,
@@ -3836,15 +3758,9 @@ describe('Keyboard interactions', () => {
<ComboboxButton>Trigger</ComboboxButton> <ComboboxButton>Trigger</ComboboxButton>
<ComboboxOptions> <ComboboxOptions>
<ComboboxOption value="a">Option A</ComboboxOption> <ComboboxOption value="a">Option A</ComboboxOption>
<ComboboxOption disabled value="b"> <ComboboxOption disabled value="b"> Option B </ComboboxOption>
Option B <ComboboxOption disabled value="c"> Option C </ComboboxOption>
</ComboboxOption> <ComboboxOption disabled value="d"> Option D </ComboboxOption>
<ComboboxOption disabled value="c">
Option C
</ComboboxOption>
<ComboboxOption disabled value="d">
Option D
</ComboboxOption>
</ComboboxOptions> </ComboboxOptions>
</Combobox> </Combobox>
`, `,
@@ -3874,18 +3790,10 @@ describe('Keyboard interactions', () => {
<ComboboxInput /> <ComboboxInput />
<ComboboxButton>Trigger</ComboboxButton> <ComboboxButton>Trigger</ComboboxButton>
<ComboboxOptions> <ComboboxOptions>
<ComboboxOption disabled value="a"> <ComboboxOption disabled value="a"> Option A </ComboboxOption>
Option A <ComboboxOption disabled value="b"> Option B </ComboboxOption>
</ComboboxOption> <ComboboxOption disabled value="c"> Option C </ComboboxOption>
<ComboboxOption disabled value="b"> <ComboboxOption disabled value="d"> Option D </ComboboxOption>
Option B
</ComboboxOption>
<ComboboxOption disabled value="c">
Option C
</ComboboxOption>
<ComboboxOption disabled value="d">
Option D
</ComboboxOption>
</ComboboxOptions> </ComboboxOptions>
</Combobox> </Combobox>
`, `,
@@ -3951,12 +3859,8 @@ describe('Keyboard interactions', () => {
<ComboboxInput /> <ComboboxInput />
<ComboboxButton>Trigger</ComboboxButton> <ComboboxButton>Trigger</ComboboxButton>
<ComboboxOptions> <ComboboxOptions>
<ComboboxOption disabled value="a"> <ComboboxOption disabled value="a"> Option A </ComboboxOption>
Option A <ComboboxOption disabled value="b"> Option B </ComboboxOption>
</ComboboxOption>
<ComboboxOption disabled value="b">
Option B
</ComboboxOption>
<ComboboxOption value="c">Option C</ComboboxOption> <ComboboxOption value="c">Option C</ComboboxOption>
<ComboboxOption value="d">Option D</ComboboxOption> <ComboboxOption value="d">Option D</ComboboxOption>
</ComboboxOptions> </ComboboxOptions>
@@ -3990,15 +3894,9 @@ describe('Keyboard interactions', () => {
<ComboboxInput /> <ComboboxInput />
<ComboboxButton>Trigger</ComboboxButton> <ComboboxButton>Trigger</ComboboxButton>
<ComboboxOptions> <ComboboxOptions>
<ComboboxOption disabled value="a"> <ComboboxOption disabled value="a"> Option A </ComboboxOption>
Option A <ComboboxOption disabled value="b"> Option B </ComboboxOption>
</ComboboxOption> <ComboboxOption disabled value="c"> Option C </ComboboxOption>
<ComboboxOption disabled value="b">
Option B
</ComboboxOption>
<ComboboxOption disabled value="c">
Option C
</ComboboxOption>
<ComboboxOption value="d">Option D</ComboboxOption> <ComboboxOption value="d">Option D</ComboboxOption>
</ComboboxOptions> </ComboboxOptions>
</Combobox> </Combobox>
@@ -4029,18 +3927,10 @@ describe('Keyboard interactions', () => {
<ComboboxInput /> <ComboboxInput />
<ComboboxButton>Trigger</ComboboxButton> <ComboboxButton>Trigger</ComboboxButton>
<ComboboxOptions> <ComboboxOptions>
<ComboboxOption disabled value="a"> <ComboboxOption disabled value="a"> Option A </ComboboxOption>
Option A <ComboboxOption disabled value="b"> Option B </ComboboxOption>
</ComboboxOption> <ComboboxOption disabled value="c"> Option C </ComboboxOption>
<ComboboxOption disabled value="b"> <ComboboxOption disabled value="d"> Option D </ComboboxOption>
Option B
</ComboboxOption>
<ComboboxOption disabled value="c">
Option C
</ComboboxOption>
<ComboboxOption disabled value="d">
Option D
</ComboboxOption>
</ComboboxOptions> </ComboboxOptions>
</Combobox> </Combobox>
`, `,
@@ -4106,12 +3996,8 @@ describe('Keyboard interactions', () => {
<ComboboxInput /> <ComboboxInput />
<ComboboxButton>Trigger</ComboboxButton> <ComboboxButton>Trigger</ComboboxButton>
<ComboboxOptions> <ComboboxOptions>
<ComboboxOption disabled value="a"> <ComboboxOption disabled value="a"> Option A </ComboboxOption>
Option A <ComboboxOption disabled value="b"> Option B </ComboboxOption>
</ComboboxOption>
<ComboboxOption disabled value="b">
Option B
</ComboboxOption>
<ComboboxOption value="c">Option C</ComboboxOption> <ComboboxOption value="c">Option C</ComboboxOption>
<ComboboxOption value="d">Option D</ComboboxOption> <ComboboxOption value="d">Option D</ComboboxOption>
</ComboboxOptions> </ComboboxOptions>
@@ -4145,15 +4031,9 @@ describe('Keyboard interactions', () => {
<ComboboxInput /> <ComboboxInput />
<ComboboxButton>Trigger</ComboboxButton> <ComboboxButton>Trigger</ComboboxButton>
<ComboboxOptions> <ComboboxOptions>
<ComboboxOption disabled value="a"> <ComboboxOption disabled value="a"> Option A </ComboboxOption>
Option A <ComboboxOption disabled value="b"> Option B </ComboboxOption>
</ComboboxOption> <ComboboxOption disabled value="c"> Option C </ComboboxOption>
<ComboboxOption disabled value="b">
Option B
</ComboboxOption>
<ComboboxOption disabled value="c">
Option C
</ComboboxOption>
<ComboboxOption value="d">Option D</ComboboxOption> <ComboboxOption value="d">Option D</ComboboxOption>
</ComboboxOptions> </ComboboxOptions>
</Combobox> </Combobox>
@@ -4184,18 +4064,10 @@ describe('Keyboard interactions', () => {
<ComboboxInput /> <ComboboxInput />
<ComboboxButton>Trigger</ComboboxButton> <ComboboxButton>Trigger</ComboboxButton>
<ComboboxOptions> <ComboboxOptions>
<ComboboxOption disabled value="a"> <ComboboxOption disabled value="a"> Option A </ComboboxOption>
Option A <ComboboxOption disabled value="b"> Option B </ComboboxOption>
</ComboboxOption> <ComboboxOption disabled value="c"> Option C </ComboboxOption>
<ComboboxOption disabled value="b"> <ComboboxOption disabled value="d"> Option D </ComboboxOption>
Option B
</ComboboxOption>
<ComboboxOption disabled value="c">
Option C
</ComboboxOption>
<ComboboxOption disabled value="d">
Option D
</ComboboxOption>
</ComboboxOptions> </ComboboxOptions>
</Combobox> </Combobox>
`, `,
@@ -4250,7 +4122,7 @@ describe('Keyboard interactions', () => {
let filteredPeople = computed(() => { let filteredPeople = computed(() => {
return query.value === '' return query.value === ''
? props.people ? props.people
: props.people.filter(person => : props.people.filter((person) =>
person.name.toLowerCase().includes(query.value.toLowerCase()) person.name.toLowerCase().includes(query.value.toLowerCase())
) )
}) })
@@ -4586,7 +4458,7 @@ describe('Mouse interactions', () => {
// Verify we have combobox options // Verify we have combobox options
let options = getComboboxOptions() let options = getComboboxOptions()
expect(options).toHaveLength(3) expect(options).toHaveLength(3)
options.forEach(option => assertComboboxOption(option)) options.forEach((option) => assertComboboxOption(option))
}) })
) )
@@ -5036,9 +4908,7 @@ describe('Mouse interactions', () => {
<ComboboxButton>Trigger</ComboboxButton> <ComboboxButton>Trigger</ComboboxButton>
<ComboboxOptions> <ComboboxOptions>
<ComboboxOption value="alice">alice</ComboboxOption> <ComboboxOption value="alice">alice</ComboboxOption>
<ComboboxOption disabled value="bob"> <ComboboxOption disabled value="bob"> bob </ComboboxOption>
bob
</ComboboxOption>
<ComboboxOption value="charlie">charlie</ComboboxOption> <ComboboxOption value="charlie">charlie</ComboboxOption>
</ComboboxOptions> </ComboboxOptions>
</Combobox> </Combobox>
@@ -5066,9 +4936,7 @@ describe('Mouse interactions', () => {
<ComboboxButton>Trigger</ComboboxButton> <ComboboxButton>Trigger</ComboboxButton>
<ComboboxOptions> <ComboboxOptions>
<ComboboxOption value="alice">alice</ComboboxOption> <ComboboxOption value="alice">alice</ComboboxOption>
<ComboboxOption disabled value="bob"> <ComboboxOption disabled value="bob"> bob </ComboboxOption>
bob
</ComboboxOption>
<ComboboxOption value="charlie">charlie</ComboboxOption> <ComboboxOption value="charlie">charlie</ComboboxOption>
</ComboboxOptions> </ComboboxOptions>
</Combobox> </Combobox>
@@ -5145,9 +5013,7 @@ describe('Mouse interactions', () => {
<ComboboxButton>Trigger</ComboboxButton> <ComboboxButton>Trigger</ComboboxButton>
<ComboboxOptions> <ComboboxOptions>
<ComboboxOption value="alice">alice</ComboboxOption> <ComboboxOption value="alice">alice</ComboboxOption>
<ComboboxOption disabled value="bob"> <ComboboxOption disabled value="bob"> bob </ComboboxOption>
bob
</ComboboxOption>
<ComboboxOption value="charlie">charlie</ComboboxOption> <ComboboxOption value="charlie">charlie</ComboboxOption>
</ComboboxOptions> </ComboboxOptions>
</Combobox> </Combobox>
@@ -5227,9 +5093,7 @@ describe('Mouse interactions', () => {
<ComboboxButton>Trigger</ComboboxButton> <ComboboxButton>Trigger</ComboboxButton>
<ComboboxOptions> <ComboboxOptions>
<ComboboxOption value="alice">alice</ComboboxOption> <ComboboxOption value="alice">alice</ComboboxOption>
<ComboboxOption disabled value="bob"> <ComboboxOption disabled value="bob"> bob </ComboboxOption>
bob
</ComboboxOption>
<ComboboxOption value="charlie">charlie</ComboboxOption> <ComboboxOption value="charlie">charlie</ComboboxOption>
</ComboboxOptions> </ComboboxOptions>
</Combobox> </Combobox>
@@ -5309,9 +5173,7 @@ describe('Mouse interactions', () => {
<ComboboxButton>Trigger</ComboboxButton> <ComboboxButton>Trigger</ComboboxButton>
<ComboboxOptions> <ComboboxOptions>
<ComboboxOption value="alice">alice</ComboboxOption> <ComboboxOption value="alice">alice</ComboboxOption>
<ComboboxOption disabled value="bob"> <ComboboxOption disabled value="bob"> bob </ComboboxOption>
bob
</ComboboxOption>
<ComboboxOption value="charlie">charlie</ComboboxOption> <ComboboxOption value="charlie">charlie</ComboboxOption>
</ComboboxOptions> </ComboboxOptions>
</Combobox> </Combobox>
@@ -131,8 +131,8 @@ export let Combobox = defineComponent({
{ {
resolveItems: () => options.value, resolveItems: () => options.value,
resolveActiveIndex: () => activeOptionIndex.value, resolveActiveIndex: () => activeOptionIndex.value,
resolveId: option => option.id, resolveId: (option) => option.id,
resolveDisabled: option => option.dataRef.disabled, resolveDisabled: (option) => option.dataRef.disabled,
} }
) )
@@ -152,7 +152,7 @@ export let Combobox = defineComponent({
} }
}, },
selectOption(id: string) { selectOption(id: string) {
let option = options.value.find(item => item.id === id) let option = options.value.find((item) => item.id === id)
if (!option) return if (!option) return
let { dataRef } = option let { dataRef } = option
@@ -193,7 +193,7 @@ export let Combobox = defineComponent({
let nextOptions = options.value.slice() let nextOptions = options.value.slice()
let currentActiveOption = let currentActiveOption =
activeOptionIndex.value !== null ? nextOptions[activeOptionIndex.value] : null activeOptionIndex.value !== null ? nextOptions[activeOptionIndex.value] : null
let idx = nextOptions.findIndex(a => a.id === id) let idx = nextOptions.findIndex((a) => a.id === id)
if (idx !== -1) nextOptions.splice(idx, 1) if (idx !== -1) nextOptions.splice(idx, 1)
options.value = nextOptions options.value = nextOptions
activeOptionIndex.value = (() => { activeOptionIndex.value = (() => {
@@ -207,7 +207,7 @@ export let Combobox = defineComponent({
}, },
} }
useWindowEvent('mousedown', event => { useWindowEvent('mousedown', (event) => {
let target = event.target as HTMLElement let target = event.target as HTMLElement
let active = document.activeElement let active = document.activeElement
@@ -8,7 +8,8 @@ import { html } from '../../test-utils/html'
import { click } from '../../test-utils/interactions' import { click } from '../../test-utils/interactions'
import { getByText } from '../../test-utils/accessibility-assertions' import { getByText } from '../../test-utils/accessibility-assertions'
function format(input: Element | string) { function format(input: Element | null | string) {
if (input === null) throw new Error('input is null')
let contents = (typeof input === 'string' ? input : (input as HTMLElement).outerHTML).trim() let contents = (typeof input === 'string' ? input : (input as HTMLElement).outerHTML).trim()
return prettier.format(contents, { parser: 'babel' }) return prettier.format(contents, { parser: 'babel' })
} }
@@ -22,59 +23,47 @@ beforeAll(() => {
afterAll(() => jest.restoreAllMocks()) afterAll(() => jest.restoreAllMocks())
function renderTemplate(input: string | Partial<Parameters<typeof defineComponent>[0]>) {
let defaultComponents = { Description }
if (typeof input === 'string') {
return render(defineComponent({ template: input, components: defaultComponents }))
}
return render(
defineComponent(
Object.assign({}, input, {
components: { ...defaultComponents, ...input.components },
}) as Parameters<typeof defineComponent>[0]
)
)
}
it('should be possible to use useDescriptions without using a Description', async () => { it('should be possible to use useDescriptions without using a Description', async () => {
let { container } = renderTemplate({ let { container } = render(
render() { defineComponent({
return h('div', [h('div', { 'aria-describedby': this.describedby }, ['No description'])]) components: { Description },
}, render() {
setup() { return h('div', [h('div', { 'aria-describedby': this.describedby }, ['No description'])])
let describedby = useDescriptions() },
return { describedby } setup() {
}, let describedby = useDescriptions()
}) return { describedby }
},
})
)
expect(format(container.firstElementChild)).toEqual( expect(format(container.firstElementChild)).toEqual(
format(html` format(html`
<div> <div>
<div> <div>No description</div>
No description
</div>
</div> </div>
`) `)
) )
}) })
it('should be possible to use useDescriptions and a single Description, and have them linked', async () => { it('should be possible to use useDescriptions and a single Description, and have them linked', async () => {
let { container } = renderTemplate({ let { container } = render(
render() { defineComponent({
return h('div', [ components: { Description },
h('div', { 'aria-describedby': this.describedby }, [ render() {
h(Description, () => 'I am a description'), return h('div', [
h('span', 'Contents'), h('div', { 'aria-describedby': this.describedby }, [
]), h(Description, () => 'I am a description'),
]) h('span', 'Contents'),
}, ]),
setup() { ])
let describedby = useDescriptions() },
return { describedby } setup() {
}, let describedby = useDescriptions()
}) return { describedby }
},
})
)
await new Promise<void>(nextTick) await new Promise<void>(nextTick)
@@ -82,12 +71,8 @@ it('should be possible to use useDescriptions and a single Description, and have
format(html` format(html`
<div> <div>
<div aria-describedby="headlessui-description-1"> <div aria-describedby="headlessui-description-1">
<p id="headlessui-description-1"> <p id="headlessui-description-1">I am a description</p>
I am a description <span>Contents</span>
</p>
<span>
Contents
</span>
</div> </div>
</div> </div>
`) `)
@@ -95,21 +80,24 @@ it('should be possible to use useDescriptions and a single Description, and have
}) })
it('should be possible to use useDescriptions and multiple Description components, and have them linked', async () => { it('should be possible to use useDescriptions and multiple Description components, and have them linked', async () => {
let { container } = renderTemplate({ let { container } = render(
render() { defineComponent({
return h('div', [ components: { Description },
h('div', { 'aria-describedby': this.describedby }, [ render() {
h(Description, () => 'I am a description'), return h('div', [
h('span', 'Contents'), h('div', { 'aria-describedby': this.describedby }, [
h(Description, () => 'I am also a description'), h(Description, () => 'I am a description'),
]), h('span', 'Contents'),
]) h(Description, () => 'I am also a description'),
}, ]),
setup() { ])
let describedby = useDescriptions() },
return { describedby } setup() {
}, let describedby = useDescriptions()
}) return { describedby }
},
})
)
await new Promise<void>(nextTick) await new Promise<void>(nextTick)
@@ -117,15 +105,9 @@ it('should be possible to use useDescriptions and multiple Description component
format(html` format(html`
<div> <div>
<div aria-describedby="headlessui-description-1 headlessui-description-2"> <div aria-describedby="headlessui-description-1 headlessui-description-2">
<p id="headlessui-description-1"> <p id="headlessui-description-1">I am a description</p>
I am a description <span>Contents</span>
</p> <p id="headlessui-description-2">I am also a description</p>
<span>
Contents
</span>
<p id="headlessui-description-2">
I am also a description
</p>
</div> </div>
</div> </div>
`) `)
@@ -133,21 +115,24 @@ it('should be possible to use useDescriptions and multiple Description component
}) })
it('should be possible to update a prop from the parent and it should reflect in the Description component', async () => { it('should be possible to update a prop from the parent and it should reflect in the Description component', async () => {
let { container } = renderTemplate({ let { container } = render(
render() { defineComponent({
return h('div', [ components: { Description },
h('div', { 'aria-describedby': this.describedby }, [ render() {
h(Description, () => 'I am a description'), return h('div', [
h('button', { onClick: () => this.count++ }, '+1'), h('div', { 'aria-describedby': this.describedby }, [
]), h(Description, () => 'I am a description'),
]) h('button', { onClick: () => this.count++ }, '+1'),
}, ]),
setup() { ])
let count = ref(0) },
let describedby = useDescriptions({ props: { 'data-count': count } }) setup() {
return { count, describedby } let count = ref(0)
}, let describedby = useDescriptions({ props: { 'data-count': count } })
}) return { count, describedby }
},
})
)
await new Promise<void>(nextTick) await new Promise<void>(nextTick)
@@ -155,9 +140,7 @@ it('should be possible to update a prop from the parent and it should reflect in
format(html` format(html`
<div> <div>
<div aria-describedby="headlessui-description-1"> <div aria-describedby="headlessui-description-1">
<p data-count="0" id="headlessui-description-1"> <p data-count="0" id="headlessui-description-1">I am a description</p>
I am a description
</p>
<button>+1</button> <button>+1</button>
</div> </div>
</div> </div>
@@ -170,9 +153,7 @@ it('should be possible to update a prop from the parent and it should reflect in
format(html` format(html`
<div> <div>
<div aria-describedby="headlessui-description-1"> <div aria-describedby="headlessui-description-1">
<p data-count="1" id="headlessui-description-1"> <p data-count="1" id="headlessui-description-1">I am a description</p>
I am a description
</p>
<button>+1</button> <button>+1</button>
</div> </div>
</div> </div>
@@ -1,4 +1,4 @@
import { defineComponent, ref, nextTick, h } from 'vue' import { defineComponent, ref, nextTick, h, ComponentOptionsWithoutProps } from 'vue'
import { render } from '../../test-utils/vue-testing-library' import { render } from '../../test-utils/vue-testing-library'
import { Dialog, DialogOverlay, DialogTitle, DialogDescription } from './dialog' import { Dialog, DialogOverlay, DialogTitle, DialogDescription } from './dialog'
@@ -30,9 +30,7 @@ afterAll(() => jest.restoreAllMocks())
let TabSentinel = defineComponent({ let TabSentinel = defineComponent({
name: 'TabSentinel', name: 'TabSentinel',
template: html` template: html` <div :tabindex="0"></div> `,
<div :tabindex="0"></div>
`,
}) })
jest.mock('../../hooks/use-id') jest.mock('../../hooks/use-id')
@@ -44,7 +42,7 @@ beforeAll(() => {
afterAll(() => jest.restoreAllMocks()) afterAll(() => jest.restoreAllMocks())
function renderTemplate(input: string | Partial<Parameters<typeof defineComponent>[0]>) { function renderTemplate(input: string | ComponentOptionsWithoutProps) {
let defaultComponents = { Dialog, DialogOverlay, DialogTitle, DialogDescription, TabSentinel } let defaultComponents = { Dialog, DialogOverlay, DialogTitle, DialogDescription, TabSentinel }
if (typeof input === 'string') { if (typeof input === 'string') {
@@ -193,7 +193,7 @@ export let Dialog = defineComponent({
provide(DialogContext, api) provide(DialogContext, api)
// Handle outside click // Handle outside click
useWindowEvent('mousedown', event => { useWindowEvent('mousedown', (event) => {
let target = event.target as HTMLElement let target = event.target as HTMLElement
if (dialogState.value !== DialogStates.Open) return if (dialogState.value !== DialogStates.Open) return
@@ -205,7 +205,7 @@ export let Dialog = defineComponent({
}) })
// Handle `Escape` to close // Handle `Escape` to close
useWindowEvent('keydown', event => { useWindowEvent('keydown', (event) => {
if (event.key !== Keys.Escape) return if (event.key !== Keys.Escape) return
if (dialogState.value !== DialogStates.Open) return if (dialogState.value !== DialogStates.Open) return
if (containers.value.size > 1) return // 1 is myself, otherwise other elements in the Stack if (containers.value.size > 1) return // 1 is myself, otherwise other elements in the Stack
@@ -215,7 +215,7 @@ export let Dialog = defineComponent({
}) })
// Scroll lock // Scroll lock
watchEffect(onInvalidate => { watchEffect((onInvalidate) => {
if (dialogState.value !== DialogStates.Open) return if (dialogState.value !== DialogStates.Open) return
let overflow = document.documentElement.style.overflow let overflow = document.documentElement.style.overflow
@@ -233,12 +233,12 @@ export let Dialog = defineComponent({
}) })
// Trigger close when the FocusTrap gets hidden // Trigger close when the FocusTrap gets hidden
watchEffect(onInvalidate => { watchEffect((onInvalidate) => {
if (dialogState.value !== DialogStates.Open) return if (dialogState.value !== DialogStates.Open) return
let container = dom(internalDialogRef) let container = dom(internalDialogRef)
if (!container) return if (!container) return
let observer = new IntersectionObserver(entries => { let observer = new IntersectionObserver((entries) => {
for (let entry of entries) { for (let entry of entries) {
if ( if (
entry.boundingClientRect.x === 0 && entry.boundingClientRect.x === 0 &&
@@ -1,4 +1,4 @@
import { defineComponent, nextTick, ref, watch, h } from 'vue' import { defineComponent, nextTick, ref, watch, h, ComponentOptionsWithoutProps } from 'vue'
import { render } from '../../test-utils/vue-testing-library' import { render } from '../../test-utils/vue-testing-library'
import { Disclosure, DisclosureButton, DisclosurePanel } from './disclosure' import { Disclosure, DisclosureButton, DisclosurePanel } from './disclosure'
import { suppressConsoleLogs } from '../../test-utils/suppress-console-logs' import { suppressConsoleLogs } from '../../test-utils/suppress-console-logs'
@@ -19,7 +19,7 @@ jest.mock('../../hooks/use-id')
afterAll(() => jest.restoreAllMocks()) afterAll(() => jest.restoreAllMocks())
function renderTemplate(input: string | Partial<Parameters<typeof defineComponent>[0]>) { function renderTemplate(input: string | ComponentOptionsWithoutProps) {
let defaultComponents = { Disclosure, DisclosureButton, DisclosurePanel } let defaultComponents = { Disclosure, DisclosureButton, DisclosurePanel }
if (typeof input === 'string') { if (typeof input === 'string') {
@@ -297,9 +297,7 @@ describe('Rendering', () => {
renderTemplate( renderTemplate(
html` html`
<Disclosure> <Disclosure>
<DisclosureButton> <DisclosureButton> Trigger </DisclosureButton>
Trigger
</DisclosureButton>
</Disclosure> </Disclosure>
` `
) )
@@ -311,9 +309,7 @@ describe('Rendering', () => {
renderTemplate( renderTemplate(
html` html`
<Disclosure> <Disclosure>
<DisclosureButton type="submit"> <DisclosureButton type="submit"> Trigger </DisclosureButton>
Trigger
</DisclosureButton>
</Disclosure> </Disclosure>
` `
) )
@@ -327,14 +323,12 @@ describe('Rendering', () => {
renderTemplate({ renderTemplate({
template: html` template: html`
<Disclosure> <Disclosure>
<DisclosureButton :as="CustomButton"> <DisclosureButton :as="CustomButton"> Trigger </DisclosureButton>
Trigger
</DisclosureButton>
</Disclosure> </Disclosure>
`, `,
setup: () => ({ setup: () => ({
CustomButton: defineComponent({ CustomButton: defineComponent({
setup: props => () => h('button', { ...props }), setup: (props) => () => h('button', { ...props }),
}), }),
}), }),
}) })
@@ -349,9 +343,7 @@ describe('Rendering', () => {
renderTemplate( renderTemplate(
html` html`
<Disclosure> <Disclosure>
<DisclosureButton as="div"> <DisclosureButton as="div"> Trigger </DisclosureButton>
Trigger
</DisclosureButton>
</Disclosure> </Disclosure>
` `
) )
@@ -365,14 +357,12 @@ describe('Rendering', () => {
renderTemplate({ renderTemplate({
template: html` template: html`
<Disclosure> <Disclosure>
<DisclosureButton :as="CustomButton"> <DisclosureButton :as="CustomButton"> Trigger </DisclosureButton>
Trigger
</DisclosureButton>
</Disclosure> </Disclosure>
`, `,
setup: () => ({ setup: () => ({
CustomButton: defineComponent({ CustomButton: defineComponent({
setup: props => () => h('div', props), setup: (props) => () => h('div', props),
}), }),
}), }),
}) })
@@ -1,4 +1,4 @@
import { defineComponent, ref, nextTick, onMounted } from 'vue' import { defineComponent, ref, nextTick, onMounted, ComponentOptionsWithoutProps } from 'vue'
import { FocusTrap } from './focus-trap' import { FocusTrap } from './focus-trap'
import { assertActiveElement, getByText } from '../../test-utils/accessibility-assertions' import { assertActiveElement, getByText } from '../../test-utils/accessibility-assertions'
@@ -16,7 +16,7 @@ beforeAll(() => {
afterAll(() => jest.restoreAllMocks()) afterAll(() => jest.restoreAllMocks())
function renderTemplate(input: string | Partial<Parameters<typeof defineComponent>[0]>) { function renderTemplate(input: string | ComponentOptionsWithoutProps) {
let defaultComponents = { FocusTrap } let defaultComponents = { FocusTrap }
if (typeof input === 'string') { if (typeof input === 'string') {
@@ -41,7 +41,7 @@ it('should focus the first focusable element inside the FocusTrap', async () =>
` `
) )
await new Promise(nextTick) await new Promise<void>(nextTick)
assertActiveElement(getByText('Trigger')) assertActiveElement(getByText('Trigger'))
}) })
@@ -64,7 +64,7 @@ it('should focus the autoFocus element inside the FocusTrap if that exists', asy
}, },
}) })
await new Promise(nextTick) await new Promise<void>(nextTick)
assertActiveElement(document.getElementById('b')) assertActiveElement(document.getElementById('b'))
}) })
@@ -84,7 +84,7 @@ it('should focus the initialFocus element inside the FocusTrap if that exists',
}, },
}) })
await new Promise(nextTick) await new Promise<void>(nextTick)
assertActiveElement(document.getElementById('c')) assertActiveElement(document.getElementById('c'))
}) })
@@ -104,7 +104,7 @@ it('should focus the initialFocus element inside the FocusTrap even if another e
}, },
}) })
await new Promise(nextTick) await new Promise<void>(nextTick)
assertActiveElement(document.getElementById('c')) assertActiveElement(document.getElementById('c'))
}) })
@@ -121,7 +121,7 @@ it('should warn when there is no focusable element inside the FocusTrap', async
` `
) )
await new Promise(nextTick) await new Promise<void>(nextTick)
expect(spy.mock.calls[0][0]).toBe('There are no focusable elements inside the <FocusTrap />') expect(spy.mock.calls[0][0]).toBe('There are no focusable elements inside the <FocusTrap />')
}) })
@@ -143,7 +143,7 @@ it(
`, `,
}) })
await new Promise(nextTick) await new Promise<void>(nextTick)
let [a, b, c, d] = Array.from(document.querySelectorAll('input')) let [a, b, c, d] = Array.from(document.querySelectorAll('input'))
@@ -193,14 +193,10 @@ it('should restore the previously focused element, before entering the FocusTrap
template: html` template: html`
<div> <div>
<input id="item-1" ref="autoFocusRef" /> <input id="item-1" ref="autoFocusRef" />
<button id="item-2" @click="visible = true"> <button id="item-2" @click="visible = true">Open modal</button>
Open modal
</button>
<FocusTrap v-if="visible"> <FocusTrap v-if="visible">
<button id="item-3" @click="visible = false"> <button id="item-3" @click="visible = false">Close</button>
Close
</button>
</FocusTrap> </FocusTrap>
</div> </div>
`, `,
@@ -214,7 +210,7 @@ it('should restore the previously focused element, before entering the FocusTrap
}, },
}) })
await new Promise(nextTick) await new Promise<void>(nextTick)
// The input should have focus by default because of the autoFocus prop // The input should have focus by default because of the autoFocus prop
assertActiveElement(document.getElementById('item-1')) assertActiveElement(document.getElementById('item-1'))
@@ -247,7 +243,7 @@ it('should be possible to tab to the next focusable element within the focus tra
` `
) )
await new Promise(nextTick) await new Promise<void>(nextTick)
// Item A should be focused because the FocusTrap will focus the first item // Item A should be focused because the FocusTrap will focus the first item
assertActiveElement(document.getElementById('item-a')) assertActiveElement(document.getElementById('item-a'))
@@ -302,12 +298,8 @@ it('should skip the initial "hidden" elements within the focus trap', async () =
<div> <div>
<button id="before">Before</button> <button id="before">Before</button>
<FocusTrap> <FocusTrap>
<button id="item-a" style="display:none"> <button id="item-a" style="display:none">Item A</button>
Item A <button id="item-b" style="display:none">Item B</button>
</button>
<button id="item-b" style="display:none">
Item B
</button>
<button id="item-c">Item C</button> <button id="item-c">Item C</button>
<button id="item-d">Item D</button> <button id="item-d">Item D</button>
</FocusTrap> </FocusTrap>
@@ -328,9 +320,7 @@ it('should be possible skip "hidden" elements within the focus trap', async () =
<FocusTrap> <FocusTrap>
<button id="item-a">Item A</button> <button id="item-a">Item A</button>
<button id="item-b">Item B</button> <button id="item-b">Item B</button>
<button id="item-c" style="display:none"> <button id="item-c" style="display:none">Item C</button>
Item C
</button>
<button id="item-d">Item D</button> <button id="item-d">Item D</button>
</FocusTrap> </FocusTrap>
<button>After</button> <button>After</button>
@@ -364,9 +354,7 @@ it('should be possible skip disabled elements within the focus trap', async () =
<FocusTrap> <FocusTrap>
<button id="item-a">Item A</button> <button id="item-a">Item A</button>
<button id="item-b">Item B</button> <button id="item-b">Item B</button>
<button id="item-c" disabled> <button id="item-c" disabled>Item C</button>
Item C
</button>
<button id="item-d">Item D</button> <button id="item-d">Item D</button>
</FocusTrap> </FocusTrap>
<button>After</button> <button>After</button>
@@ -8,7 +8,8 @@ import { html } from '../../test-utils/html'
import { click } from '../../test-utils/interactions' import { click } from '../../test-utils/interactions'
import { getByText } from '../../test-utils/accessibility-assertions' import { getByText } from '../../test-utils/accessibility-assertions'
function format(input: Element | string) { function format(input: Element | null | string) {
if (input === null) throw new Error('input is null')
let contents = (typeof input === 'string' ? input : (input as HTMLElement).outerHTML).trim() let contents = (typeof input === 'string' ? input : (input as HTMLElement).outerHTML).trim()
return prettier.format(contents, { parser: 'babel' }) return prettier.format(contents, { parser: 'babel' })
} }
@@ -22,59 +23,47 @@ beforeAll(() => {
afterAll(() => jest.restoreAllMocks()) afterAll(() => jest.restoreAllMocks())
function renderTemplate(input: string | Partial<Parameters<typeof defineComponent>[0]>) {
let defaultComponents = { Label }
if (typeof input === 'string') {
return render(defineComponent({ template: input, components: defaultComponents }))
}
return render(
defineComponent(
Object.assign({}, input, {
components: { ...defaultComponents, ...input.components },
}) as Parameters<typeof defineComponent>[0]
)
)
}
it('should be possible to use useLabels without using a Label', async () => { it('should be possible to use useLabels without using a Label', async () => {
let { container } = renderTemplate({ let { container } = render(
render() { defineComponent({
return h('div', [h('div', { 'aria-labelledby': this.labelledby }, ['No label'])]) components: { Label },
}, render() {
setup() { return h('div', [h('div', { 'aria-labelledby': this.labelledby }, ['No label'])])
let labelledby = useLabels() },
return { labelledby } setup() {
}, let labelledby = useLabels()
}) return { labelledby }
},
})
)
expect(format(container.firstElementChild)).toEqual( expect(format(container.firstElementChild)).toEqual(
format(html` format(html`
<div> <div>
<div> <div>No label</div>
No label
</div>
</div> </div>
`) `)
) )
}) })
it('should be possible to use useLabels and a single Label, and have them linked', async () => { it('should be possible to use useLabels and a single Label, and have them linked', async () => {
let { container } = renderTemplate({ let { container } = render(
render() { defineComponent({
return h('div', [ components: { Label },
h('div', { 'aria-labelledby': this.labelledby }, [ render() {
h(Label, () => 'I am a label'), return h('div', [
h('span', 'Contents'), h('div', { 'aria-labelledby': this.labelledby }, [
]), h(Label, () => 'I am a label'),
]) h('span', 'Contents'),
}, ]),
setup() { ])
let labelledby = useLabels() },
return { labelledby } setup() {
}, let labelledby = useLabels()
}) return { labelledby }
},
})
)
await new Promise<void>(nextTick) await new Promise<void>(nextTick)
@@ -82,12 +71,8 @@ it('should be possible to use useLabels and a single Label, and have them linked
format(html` format(html`
<div> <div>
<div aria-labelledby="headlessui-label-1"> <div aria-labelledby="headlessui-label-1">
<label id="headlessui-label-1"> <label id="headlessui-label-1">I am a label</label>
I am a label <span>Contents</span>
</label>
<span>
Contents
</span>
</div> </div>
</div> </div>
`) `)
@@ -95,21 +80,24 @@ it('should be possible to use useLabels and a single Label, and have them linked
}) })
it('should be possible to use useLabels and multiple Label components, and have them linked', async () => { it('should be possible to use useLabels and multiple Label components, and have them linked', async () => {
let { container } = renderTemplate({ let { container } = render(
render() { defineComponent({
return h('div', [ components: { Label },
h('div', { 'aria-labelledby': this.labelledby }, [ render() {
h(Label, () => 'I am a label'), return h('div', [
h('span', 'Contents'), h('div', { 'aria-labelledby': this.labelledby }, [
h(Label, () => 'I am also a label'), h(Label, () => 'I am a label'),
]), h('span', 'Contents'),
]) h(Label, () => 'I am also a label'),
}, ]),
setup() { ])
let labelledby = useLabels() },
return { labelledby } setup() {
}, let labelledby = useLabels()
}) return { labelledby }
},
})
)
await new Promise<void>(nextTick) await new Promise<void>(nextTick)
@@ -117,15 +105,9 @@ it('should be possible to use useLabels and multiple Label components, and have
format(html` format(html`
<div> <div>
<div aria-labelledby="headlessui-label-1 headlessui-label-2"> <div aria-labelledby="headlessui-label-1 headlessui-label-2">
<label id="headlessui-label-1"> <label id="headlessui-label-1">I am a label</label>
I am a label <span>Contents</span>
</label> <label id="headlessui-label-2">I am also a label</label>
<span>
Contents
</span>
<label id="headlessui-label-2">
I am also a label
</label>
</div> </div>
</div> </div>
`) `)
@@ -133,21 +115,24 @@ it('should be possible to use useLabels and multiple Label components, and have
}) })
it('should be possible to update a prop from the parent and it should reflect in the Label component', async () => { it('should be possible to update a prop from the parent and it should reflect in the Label component', async () => {
let { container } = renderTemplate({ let { container } = render(
render() { defineComponent({
return h('div', [ components: { Label },
h('div', { 'aria-labelledby': this.labelledby }, [ render() {
h(Label, () => 'I am a label'), return h('div', [
h('button', { onClick: () => this.count++ }, '+1'), h('div', { 'aria-labelledby': this.labelledby }, [
]), h(Label, () => 'I am a label'),
]) h('button', { onClick: () => this.count++ }, '+1'),
}, ]),
setup() { ])
let count = ref(0) },
let labelledby = useLabels({ props: { 'data-count': count } }) setup() {
return { count, labelledby } let count = ref(0)
}, let labelledby = useLabels({ props: { 'data-count': count } })
}) return { count, labelledby }
},
})
)
await new Promise<void>(nextTick) await new Promise<void>(nextTick)
@@ -155,9 +140,7 @@ it('should be possible to update a prop from the parent and it should reflect in
format(html` format(html`
<div> <div>
<div aria-labelledby="headlessui-label-1"> <div aria-labelledby="headlessui-label-1">
<label data-count="0" id="headlessui-label-1"> <label data-count="0" id="headlessui-label-1">I am a label</label>
I am a label
</label>
<button>+1</button> <button>+1</button>
</div> </div>
</div> </div>
@@ -170,9 +153,7 @@ it('should be possible to update a prop from the parent and it should reflect in
format(html` format(html`
<div> <div>
<div aria-labelledby="headlessui-label-1"> <div aria-labelledby="headlessui-label-1">
<label data-count="1" id="headlessui-label-1"> <label data-count="1" id="headlessui-label-1">I am a label</label>
I am a label
</label>
<button>+1</button> <button>+1</button>
</div> </div>
</div> </div>
@@ -1,4 +1,12 @@
import { defineComponent, nextTick, ref, watch, h, reactive } from 'vue' import {
defineComponent,
nextTick,
ref,
watch,
h,
reactive,
ComponentOptionsWithoutProps,
} from 'vue'
import { render } from '../../test-utils/vue-testing-library' import { render } from '../../test-utils/vue-testing-library'
import { Listbox, ListboxLabel, ListboxButton, ListboxOptions, ListboxOption } from './listbox' import { Listbox, ListboxLabel, ListboxButton, ListboxOptions, ListboxOption } from './listbox'
import { suppressConsoleLogs } from '../../test-utils/suppress-console-logs' import { suppressConsoleLogs } from '../../test-utils/suppress-console-logs'
@@ -48,7 +56,7 @@ beforeAll(() => {
afterAll(() => jest.restoreAllMocks()) afterAll(() => jest.restoreAllMocks())
function nextFrame() { function nextFrame() {
return new Promise(resolve => { return new Promise<void>((resolve) => {
requestAnimationFrame(() => { requestAnimationFrame(() => {
requestAnimationFrame(() => { requestAnimationFrame(() => {
resolve() resolve()
@@ -57,7 +65,7 @@ function nextFrame() {
}) })
} }
function renderTemplate(input: string | Partial<Parameters<typeof defineComponent>[0]>) { function renderTemplate(input: string | ComponentOptionsWithoutProps) {
let defaultComponents = { Listbox, ListboxLabel, ListboxButton, ListboxOptions, ListboxOption } let defaultComponents = { Listbox, ListboxLabel, ListboxButton, ListboxOptions, ListboxOption }
if (typeof input === 'string') { if (typeof input === 'string') {
@@ -388,9 +396,7 @@ describe('Rendering', () => {
renderTemplate({ renderTemplate({
template: html` template: html`
<Listbox v-model="value"> <Listbox v-model="value">
<ListboxButton type="submit"> <ListboxButton type="submit"> Trigger </ListboxButton>
Trigger
</ListboxButton>
</Listbox> </Listbox>
`, `,
setup: () => ({ value: ref(null) }), setup: () => ({ value: ref(null) }),
@@ -405,15 +411,13 @@ describe('Rendering', () => {
renderTemplate({ renderTemplate({
template: html` template: html`
<Listbox v-model="value"> <Listbox v-model="value">
<ListboxButton :as="CustomButton"> <ListboxButton :as="CustomButton"> Trigger </ListboxButton>
Trigger
</ListboxButton>
</Listbox> </Listbox>
`, `,
setup: () => ({ setup: () => ({
value: ref(null), value: ref(null),
CustomButton: defineComponent({ CustomButton: defineComponent({
setup: props => () => h('button', { ...props }), setup: (props) => () => h('button', { ...props }),
}), }),
}), }),
}) })
@@ -428,9 +432,7 @@ describe('Rendering', () => {
renderTemplate({ renderTemplate({
template: html` template: html`
<Listbox v-model="value"> <Listbox v-model="value">
<ListboxButton as="div"> <ListboxButton as="div"> Trigger </ListboxButton>
Trigger
</ListboxButton>
</Listbox> </Listbox>
`, `,
setup: () => ({ value: ref(null) }), setup: () => ({ value: ref(null) }),
@@ -445,15 +447,13 @@ describe('Rendering', () => {
renderTemplate({ renderTemplate({
template: html` template: html`
<Listbox v-model="value"> <Listbox v-model="value">
<ListboxButton :as="CustomButton"> <ListboxButton :as="CustomButton"> Trigger </ListboxButton>
Trigger
</ListboxButton>
</Listbox> </Listbox>
`, `,
setup: () => ({ setup: () => ({
value: ref(null), value: ref(null),
CustomButton: defineComponent({ CustomButton: defineComponent({
setup: props => () => h('div', props), setup: (props) => () => h('div', props),
}), }),
}), }),
}) })
@@ -647,15 +647,9 @@ describe('Rendering composition', () => {
<Listbox v-model="value"> <Listbox v-model="value">
<ListboxButton>Trigger</ListboxButton> <ListboxButton>Trigger</ListboxButton>
<ListboxOptions> <ListboxOptions>
<ListboxOption as="button" value="a"> <ListboxOption as="button" value="a"> Option A </ListboxOption>
Option A <ListboxOption as="button" value="b"> Option B </ListboxOption>
</ListboxOption> <ListboxOption as="button" value="c"> Option C </ListboxOption>
<ListboxOption as="button" value="b">
Option B
</ListboxOption>
<ListboxOption as="button" value="c">
Option C
</ListboxOption>
</ListboxOptions> </ListboxOptions>
</Listbox> </Listbox>
`, `,
@@ -672,7 +666,7 @@ describe('Rendering composition', () => {
await click(getListboxButton()) await click(getListboxButton())
// Verify options are buttons now // Verify options are buttons now
getListboxOptions().forEach(option => assertListboxOption(option, { tag: 'button' })) getListboxOptions().forEach((option) => assertListboxOption(option, { tag: 'button' }))
}) })
) )
}) })
@@ -704,9 +698,7 @@ describe('Composition', () => {
<Listbox> <Listbox>
<ListboxButton>Trigger</ListboxButton> <ListboxButton>Trigger</ListboxButton>
<OpenClosedWrite :open="true"> <OpenClosedWrite :open="true">
<ListboxOptions v-slot="data"> <ListboxOptions v-slot="data"> {{JSON.stringify(data)}} </ListboxOptions>
{{JSON.stringify(data)}}
</ListboxOptions>
</OpenClosedWrite> </OpenClosedWrite>
</Listbox> </Listbox>
`, `,
@@ -734,9 +726,7 @@ describe('Composition', () => {
<Listbox> <Listbox>
<ListboxButton>Trigger</ListboxButton> <ListboxButton>Trigger</ListboxButton>
<OpenClosedWrite :open="false"> <OpenClosedWrite :open="false">
<ListboxOptions v-slot="data"> <ListboxOptions v-slot="data"> {{JSON.stringify(data)}} </ListboxOptions>
{{JSON.stringify(data)}}
</ListboxOptions>
</OpenClosedWrite> </OpenClosedWrite>
</Listbox> </Listbox>
`, `,
@@ -840,7 +830,7 @@ describe('Keyboard interactions', () => {
// Verify we have listbox options // Verify we have listbox options
let options = getListboxOptions() let options = getListboxOptions()
expect(options).toHaveLength(3) expect(options).toHaveLength(3)
options.forEach(option => assertListboxOption(option, { selected: false })) options.forEach((option) => assertListboxOption(option, { selected: false }))
// Verify that the first listbox option is active // Verify that the first listbox option is active
assertActiveListboxOption(options[0]) assertActiveListboxOption(options[0])
@@ -1092,9 +1082,7 @@ describe('Keyboard interactions', () => {
<Listbox v-model="value"> <Listbox v-model="value">
<ListboxButton>Trigger</ListboxButton> <ListboxButton>Trigger</ListboxButton>
<ListboxOptions> <ListboxOptions>
<ListboxOption disabled value="a"> <ListboxOption disabled value="a"> Option A </ListboxOption>
Option A
</ListboxOption>
<ListboxOption value="b">Option B</ListboxOption> <ListboxOption value="b">Option B</ListboxOption>
<ListboxOption value="c">Option C</ListboxOption> <ListboxOption value="c">Option C</ListboxOption>
</ListboxOptions> </ListboxOptions>
@@ -1130,12 +1118,8 @@ describe('Keyboard interactions', () => {
<Listbox v-model="value"> <Listbox v-model="value">
<ListboxButton>Trigger</ListboxButton> <ListboxButton>Trigger</ListboxButton>
<ListboxOptions> <ListboxOptions>
<ListboxOption disabled value="a"> <ListboxOption disabled value="a"> Option A </ListboxOption>
Option A <ListboxOption disabled value="b"> Option B </ListboxOption>
</ListboxOption>
<ListboxOption disabled value="b">
Option B
</ListboxOption>
<ListboxOption value="c">Option C</ListboxOption> <ListboxOption value="c">Option C</ListboxOption>
</ListboxOptions> </ListboxOptions>
</Listbox> </Listbox>
@@ -1170,15 +1154,9 @@ describe('Keyboard interactions', () => {
<Listbox v-model="value"> <Listbox v-model="value">
<ListboxButton>Trigger</ListboxButton> <ListboxButton>Trigger</ListboxButton>
<ListboxOptions> <ListboxOptions>
<ListboxOption disabled value="a"> <ListboxOption disabled value="a"> Option A </ListboxOption>
Option A <ListboxOption disabled value="b"> Option B </ListboxOption>
</ListboxOption> <ListboxOption disabled value="c"> Option C </ListboxOption>
<ListboxOption disabled value="b">
Option B
</ListboxOption>
<ListboxOption disabled value="c">
Option C
</ListboxOption>
</ListboxOptions> </ListboxOptions>
</Listbox> </Listbox>
`, `,
@@ -1345,7 +1323,7 @@ describe('Keyboard interactions', () => {
// Verify we have listbox options // Verify we have listbox options
let options = getListboxOptions() let options = getListboxOptions()
expect(options).toHaveLength(3) expect(options).toHaveLength(3)
options.forEach(option => assertListboxOption(option)) options.forEach((option) => assertListboxOption(option))
assertActiveListboxOption(options[0]) assertActiveListboxOption(options[0])
}) })
) )
@@ -1471,9 +1449,7 @@ describe('Keyboard interactions', () => {
<Listbox v-model="value"> <Listbox v-model="value">
<ListboxButton>Trigger</ListboxButton> <ListboxButton>Trigger</ListboxButton>
<ListboxOptions> <ListboxOptions>
<ListboxOption disabled value="a"> <ListboxOption disabled value="a"> Option A </ListboxOption>
Option A
</ListboxOption>
<ListboxOption value="b">Option B</ListboxOption> <ListboxOption value="b">Option B</ListboxOption>
<ListboxOption value="c">Option C</ListboxOption> <ListboxOption value="c">Option C</ListboxOption>
</ListboxOptions> </ListboxOptions>
@@ -1509,12 +1485,8 @@ describe('Keyboard interactions', () => {
<Listbox v-model="value"> <Listbox v-model="value">
<ListboxButton>Trigger</ListboxButton> <ListboxButton>Trigger</ListboxButton>
<ListboxOptions> <ListboxOptions>
<ListboxOption disabled value="a"> <ListboxOption disabled value="a"> Option A </ListboxOption>
Option A <ListboxOption disabled value="b"> Option B </ListboxOption>
</ListboxOption>
<ListboxOption disabled value="b">
Option B
</ListboxOption>
<ListboxOption value="c">Option C</ListboxOption> <ListboxOption value="c">Option C</ListboxOption>
</ListboxOptions> </ListboxOptions>
</Listbox> </Listbox>
@@ -1549,15 +1521,9 @@ describe('Keyboard interactions', () => {
<Listbox v-model="value"> <Listbox v-model="value">
<ListboxButton>Trigger</ListboxButton> <ListboxButton>Trigger</ListboxButton>
<ListboxOptions> <ListboxOptions>
<ListboxOption disabled value="a"> <ListboxOption disabled value="a"> Option A </ListboxOption>
Option A <ListboxOption disabled value="b"> Option B </ListboxOption>
</ListboxOption> <ListboxOption disabled value="c"> Option C </ListboxOption>
<ListboxOption disabled value="b">
Option B
</ListboxOption>
<ListboxOption disabled value="c">
Option C
</ListboxOption>
</ListboxOptions> </ListboxOptions>
</Listbox> </Listbox>
`, `,
@@ -1729,7 +1695,7 @@ describe('Keyboard interactions', () => {
// Verify we have listbox options // Verify we have listbox options
let options = getListboxOptions() let options = getListboxOptions()
expect(options).toHaveLength(3) expect(options).toHaveLength(3)
options.forEach(option => assertListboxOption(option)) options.forEach((option) => assertListboxOption(option))
assertActiveListboxOption(options[0]) assertActiveListboxOption(options[0])
// Try to tab // Try to tab
@@ -1783,7 +1749,7 @@ describe('Keyboard interactions', () => {
// Verify we have listbox options // Verify we have listbox options
let options = getListboxOptions() let options = getListboxOptions()
expect(options).toHaveLength(3) expect(options).toHaveLength(3)
options.forEach(option => assertListboxOption(option)) options.forEach((option) => assertListboxOption(option))
assertActiveListboxOption(options[0]) assertActiveListboxOption(options[0])
// Try to Shift+Tab // Try to Shift+Tab
@@ -1839,7 +1805,7 @@ describe('Keyboard interactions', () => {
// Verify we have listbox options // Verify we have listbox options
let options = getListboxOptions() let options = getListboxOptions()
expect(options).toHaveLength(3) expect(options).toHaveLength(3)
options.forEach(option => assertListboxOption(option)) options.forEach((option) => assertListboxOption(option))
// Verify that the first listbox option is active // Verify that the first listbox option is active
assertActiveListboxOption(options[0]) assertActiveListboxOption(options[0])
@@ -1991,7 +1957,7 @@ describe('Keyboard interactions', () => {
// Verify we have listbox options // Verify we have listbox options
let options = getListboxOptions() let options = getListboxOptions()
expect(options).toHaveLength(3) expect(options).toHaveLength(3)
options.forEach(option => assertListboxOption(option)) options.forEach((option) => assertListboxOption(option))
assertActiveListboxOption(options[0]) assertActiveListboxOption(options[0])
// We should be able to go down once // We should be able to go down once
@@ -2016,9 +1982,7 @@ describe('Keyboard interactions', () => {
<Listbox v-model="value"> <Listbox v-model="value">
<ListboxButton>Trigger</ListboxButton> <ListboxButton>Trigger</ListboxButton>
<ListboxOptions> <ListboxOptions>
<ListboxOption disabled value="a"> <ListboxOption disabled value="a"> Option A </ListboxOption>
Option A
</ListboxOption>
<ListboxOption value="b">Option B</ListboxOption> <ListboxOption value="b">Option B</ListboxOption>
<ListboxOption value="c">Option C</ListboxOption> <ListboxOption value="c">Option C</ListboxOption>
</ListboxOptions> </ListboxOptions>
@@ -2042,7 +2006,7 @@ describe('Keyboard interactions', () => {
// Verify we have listbox options // Verify we have listbox options
let options = getListboxOptions() let options = getListboxOptions()
expect(options).toHaveLength(3) expect(options).toHaveLength(3)
options.forEach(option => assertListboxOption(option)) options.forEach((option) => assertListboxOption(option))
assertActiveListboxOption(options[1]) assertActiveListboxOption(options[1])
// We should be able to go down once // We should be able to go down once
@@ -2059,12 +2023,8 @@ describe('Keyboard interactions', () => {
<Listbox v-model="value"> <Listbox v-model="value">
<ListboxButton>Trigger</ListboxButton> <ListboxButton>Trigger</ListboxButton>
<ListboxOptions> <ListboxOptions>
<ListboxOption disabled value="a"> <ListboxOption disabled value="a"> Option A </ListboxOption>
Option A <ListboxOption disabled value="b"> Option B </ListboxOption>
</ListboxOption>
<ListboxOption disabled value="b">
Option B
</ListboxOption>
<ListboxOption value="c">Option C</ListboxOption> <ListboxOption value="c">Option C</ListboxOption>
</ListboxOptions> </ListboxOptions>
</Listbox> </Listbox>
@@ -2087,7 +2047,7 @@ describe('Keyboard interactions', () => {
// Verify we have listbox options // Verify we have listbox options
let options = getListboxOptions() let options = getListboxOptions()
expect(options).toHaveLength(3) expect(options).toHaveLength(3)
options.forEach(option => assertListboxOption(option)) options.forEach((option) => assertListboxOption(option))
assertActiveListboxOption(options[2]) assertActiveListboxOption(options[2])
}) })
) )
@@ -2126,7 +2086,7 @@ describe('Keyboard interactions', () => {
// Verify we have listbox options // Verify we have listbox options
let options = getListboxOptions() let options = getListboxOptions()
expect(options).toHaveLength(3) expect(options).toHaveLength(3)
options.forEach(option => assertListboxOption(option)) options.forEach((option) => assertListboxOption(option))
assertActiveListboxOption(options[0]) assertActiveListboxOption(options[0])
// We should be able to go right once // We should be able to go right once
@@ -2186,7 +2146,7 @@ describe('Keyboard interactions', () => {
// Verify we have listbox options // Verify we have listbox options
let options = getListboxOptions() let options = getListboxOptions()
expect(options).toHaveLength(3) expect(options).toHaveLength(3)
options.forEach(option => assertListboxOption(option)) options.forEach((option) => assertListboxOption(option))
// ! ALERT: The LAST option should now be active // ! ALERT: The LAST option should now be active
assertActiveListboxOption(options[2]) assertActiveListboxOption(options[2])
@@ -2315,12 +2275,8 @@ describe('Keyboard interactions', () => {
<ListboxButton>Trigger</ListboxButton> <ListboxButton>Trigger</ListboxButton>
<ListboxOptions> <ListboxOptions>
<ListboxOption value="a">Option A</ListboxOption> <ListboxOption value="a">Option A</ListboxOption>
<ListboxOption disabled value="b"> <ListboxOption disabled value="b"> Option B </ListboxOption>
Option B <ListboxOption disabled value="c"> Option C </ListboxOption>
</ListboxOption>
<ListboxOption disabled value="c">
Option C
</ListboxOption>
</ListboxOptions> </ListboxOptions>
</Listbox> </Listbox>
`, `,
@@ -2342,7 +2298,7 @@ describe('Keyboard interactions', () => {
// Verify we have listbox options // Verify we have listbox options
let options = getListboxOptions() let options = getListboxOptions()
expect(options).toHaveLength(3) expect(options).toHaveLength(3)
options.forEach(option => assertListboxOption(option)) options.forEach((option) => assertListboxOption(option))
assertActiveListboxOption(options[0]) assertActiveListboxOption(options[0])
}) })
) )
@@ -2355,12 +2311,8 @@ describe('Keyboard interactions', () => {
<Listbox v-model="value"> <Listbox v-model="value">
<ListboxButton>Trigger</ListboxButton> <ListboxButton>Trigger</ListboxButton>
<ListboxOptions> <ListboxOptions>
<ListboxOption disabled value="a"> <ListboxOption disabled value="a"> Option A </ListboxOption>
Option A <ListboxOption disabled value="b"> Option B </ListboxOption>
</ListboxOption>
<ListboxOption disabled value="b">
Option B
</ListboxOption>
<ListboxOption value="c">Option C</ListboxOption> <ListboxOption value="c">Option C</ListboxOption>
</ListboxOptions> </ListboxOptions>
</Listbox> </Listbox>
@@ -2383,7 +2335,7 @@ describe('Keyboard interactions', () => {
// Verify we have listbox options // Verify we have listbox options
let options = getListboxOptions() let options = getListboxOptions()
expect(options).toHaveLength(3) expect(options).toHaveLength(3)
options.forEach(option => assertListboxOption(option)) options.forEach((option) => assertListboxOption(option))
assertActiveListboxOption(options[2]) assertActiveListboxOption(options[2])
// We should not be able to go up (because those are disabled) // We should not be able to go up (because those are disabled)
@@ -2437,7 +2389,7 @@ describe('Keyboard interactions', () => {
// Verify we have listbox options // Verify we have listbox options
let options = getListboxOptions() let options = getListboxOptions()
expect(options).toHaveLength(3) expect(options).toHaveLength(3)
options.forEach(option => assertListboxOption(option)) options.forEach((option) => assertListboxOption(option))
assertActiveListboxOption(options[2]) assertActiveListboxOption(options[2])
// We should be able to go down once // We should be able to go down once
@@ -2498,7 +2450,7 @@ describe('Keyboard interactions', () => {
// Verify we have listbox options // Verify we have listbox options
let options = getListboxOptions() let options = getListboxOptions()
expect(options).toHaveLength(3) expect(options).toHaveLength(3)
options.forEach(option => assertListboxOption(option)) options.forEach((option) => assertListboxOption(option))
assertActiveListboxOption(options[2]) assertActiveListboxOption(options[2])
// We should be able to go left once // We should be able to go left once
@@ -2561,12 +2513,8 @@ describe('Keyboard interactions', () => {
<ListboxOptions> <ListboxOptions>
<ListboxOption value="a">Option A</ListboxOption> <ListboxOption value="a">Option A</ListboxOption>
<ListboxOption value="b">Option B</ListboxOption> <ListboxOption value="b">Option B</ListboxOption>
<ListboxOption disabled value="c"> <ListboxOption disabled value="c"> Option C </ListboxOption>
Option C <ListboxOption disabled value="d"> Option D </ListboxOption>
</ListboxOption>
<ListboxOption disabled value="d">
Option D
</ListboxOption>
</ListboxOptions> </ListboxOptions>
</Listbox> </Listbox>
`, `,
@@ -2599,15 +2547,9 @@ describe('Keyboard interactions', () => {
<ListboxButton>Trigger</ListboxButton> <ListboxButton>Trigger</ListboxButton>
<ListboxOptions> <ListboxOptions>
<ListboxOption value="a">Option A</ListboxOption> <ListboxOption value="a">Option A</ListboxOption>
<ListboxOption disabled value="b"> <ListboxOption disabled value="b"> Option B </ListboxOption>
Option B <ListboxOption disabled value="c"> Option C </ListboxOption>
</ListboxOption> <ListboxOption disabled value="d"> Option D </ListboxOption>
<ListboxOption disabled value="c">
Option C
</ListboxOption>
<ListboxOption disabled value="d">
Option D
</ListboxOption>
</ListboxOptions> </ListboxOptions>
</Listbox> </Listbox>
`, `,
@@ -2636,18 +2578,10 @@ describe('Keyboard interactions', () => {
<Listbox v-model="value"> <Listbox v-model="value">
<ListboxButton>Trigger</ListboxButton> <ListboxButton>Trigger</ListboxButton>
<ListboxOptions> <ListboxOptions>
<ListboxOption disabled value="a"> <ListboxOption disabled value="a"> Option A </ListboxOption>
Option A <ListboxOption disabled value="b"> Option B </ListboxOption>
</ListboxOption> <ListboxOption disabled value="c"> Option C </ListboxOption>
<ListboxOption disabled value="b"> <ListboxOption disabled value="d"> Option D </ListboxOption>
Option B
</ListboxOption>
<ListboxOption disabled value="c">
Option C
</ListboxOption>
<ListboxOption disabled value="d">
Option D
</ListboxOption>
</ListboxOptions> </ListboxOptions>
</Listbox> </Listbox>
`, `,
@@ -2713,12 +2647,8 @@ describe('Keyboard interactions', () => {
<ListboxOptions> <ListboxOptions>
<ListboxOption value="a">Option A</ListboxOption> <ListboxOption value="a">Option A</ListboxOption>
<ListboxOption value="b">Option B</ListboxOption> <ListboxOption value="b">Option B</ListboxOption>
<ListboxOption disabled value="c"> <ListboxOption disabled value="c"> Option C </ListboxOption>
Option C <ListboxOption disabled value="d"> Option D </ListboxOption>
</ListboxOption>
<ListboxOption disabled value="d">
Option D
</ListboxOption>
</ListboxOptions> </ListboxOptions>
</Listbox> </Listbox>
`, `,
@@ -2751,15 +2681,9 @@ describe('Keyboard interactions', () => {
<ListboxButton>Trigger</ListboxButton> <ListboxButton>Trigger</ListboxButton>
<ListboxOptions> <ListboxOptions>
<ListboxOption value="a">Option A</ListboxOption> <ListboxOption value="a">Option A</ListboxOption>
<ListboxOption disabled value="b"> <ListboxOption disabled value="b"> Option B </ListboxOption>
Option B <ListboxOption disabled value="c"> Option C </ListboxOption>
</ListboxOption> <ListboxOption disabled value="d"> Option D </ListboxOption>
<ListboxOption disabled value="c">
Option C
</ListboxOption>
<ListboxOption disabled value="d">
Option D
</ListboxOption>
</ListboxOptions> </ListboxOptions>
</Listbox> </Listbox>
`, `,
@@ -2788,18 +2712,10 @@ describe('Keyboard interactions', () => {
<Listbox v-model="value"> <Listbox v-model="value">
<ListboxButton>Trigger</ListboxButton> <ListboxButton>Trigger</ListboxButton>
<ListboxOptions> <ListboxOptions>
<ListboxOption disabled value="a"> <ListboxOption disabled value="a"> Option A </ListboxOption>
Option A <ListboxOption disabled value="b"> Option B </ListboxOption>
</ListboxOption> <ListboxOption disabled value="c"> Option C </ListboxOption>
<ListboxOption disabled value="b"> <ListboxOption disabled value="d"> Option D </ListboxOption>
Option B
</ListboxOption>
<ListboxOption disabled value="c">
Option C
</ListboxOption>
<ListboxOption disabled value="d">
Option D
</ListboxOption>
</ListboxOptions> </ListboxOptions>
</Listbox> </Listbox>
`, `,
@@ -2863,12 +2779,8 @@ describe('Keyboard interactions', () => {
<Listbox v-model="value"> <Listbox v-model="value">
<ListboxButton>Trigger</ListboxButton> <ListboxButton>Trigger</ListboxButton>
<ListboxOptions> <ListboxOptions>
<ListboxOption disabled value="a"> <ListboxOption disabled value="a"> Option A </ListboxOption>
Option A <ListboxOption disabled value="b"> Option B </ListboxOption>
</ListboxOption>
<ListboxOption disabled value="b">
Option B
</ListboxOption>
<ListboxOption value="c">Option C</ListboxOption> <ListboxOption value="c">Option C</ListboxOption>
<ListboxOption value="d">Option D</ListboxOption> <ListboxOption value="d">Option D</ListboxOption>
</ListboxOptions> </ListboxOptions>
@@ -2901,15 +2813,9 @@ describe('Keyboard interactions', () => {
<Listbox v-model="value"> <Listbox v-model="value">
<ListboxButton>Trigger</ListboxButton> <ListboxButton>Trigger</ListboxButton>
<ListboxOptions> <ListboxOptions>
<ListboxOption disabled value="a"> <ListboxOption disabled value="a"> Option A </ListboxOption>
Option A <ListboxOption disabled value="b"> Option B </ListboxOption>
</ListboxOption> <ListboxOption disabled value="c"> Option C </ListboxOption>
<ListboxOption disabled value="b">
Option B
</ListboxOption>
<ListboxOption disabled value="c">
Option C
</ListboxOption>
<ListboxOption value="d">Option D</ListboxOption> <ListboxOption value="d">Option D</ListboxOption>
</ListboxOptions> </ListboxOptions>
</Listbox> </Listbox>
@@ -2939,18 +2845,10 @@ describe('Keyboard interactions', () => {
<Listbox v-model="value"> <Listbox v-model="value">
<ListboxButton>Trigger</ListboxButton> <ListboxButton>Trigger</ListboxButton>
<ListboxOptions> <ListboxOptions>
<ListboxOption disabled value="a"> <ListboxOption disabled value="a"> Option A </ListboxOption>
Option A <ListboxOption disabled value="b"> Option B </ListboxOption>
</ListboxOption> <ListboxOption disabled value="c"> Option C </ListboxOption>
<ListboxOption disabled value="b"> <ListboxOption disabled value="d"> Option D </ListboxOption>
Option B
</ListboxOption>
<ListboxOption disabled value="c">
Option C
</ListboxOption>
<ListboxOption disabled value="d">
Option D
</ListboxOption>
</ListboxOptions> </ListboxOptions>
</Listbox> </Listbox>
`, `,
@@ -3014,12 +2912,8 @@ describe('Keyboard interactions', () => {
<Listbox v-model="value"> <Listbox v-model="value">
<ListboxButton>Trigger</ListboxButton> <ListboxButton>Trigger</ListboxButton>
<ListboxOptions> <ListboxOptions>
<ListboxOption disabled value="a"> <ListboxOption disabled value="a"> Option A </ListboxOption>
Option A <ListboxOption disabled value="b"> Option B </ListboxOption>
</ListboxOption>
<ListboxOption disabled value="b">
Option B
</ListboxOption>
<ListboxOption value="c">Option C</ListboxOption> <ListboxOption value="c">Option C</ListboxOption>
<ListboxOption value="d">Option D</ListboxOption> <ListboxOption value="d">Option D</ListboxOption>
</ListboxOptions> </ListboxOptions>
@@ -3052,15 +2946,9 @@ describe('Keyboard interactions', () => {
<Listbox v-model="value"> <Listbox v-model="value">
<ListboxButton>Trigger</ListboxButton> <ListboxButton>Trigger</ListboxButton>
<ListboxOptions> <ListboxOptions>
<ListboxOption disabled value="a"> <ListboxOption disabled value="a"> Option A </ListboxOption>
Option A <ListboxOption disabled value="b"> Option B </ListboxOption>
</ListboxOption> <ListboxOption disabled value="c"> Option C </ListboxOption>
<ListboxOption disabled value="b">
Option B
</ListboxOption>
<ListboxOption disabled value="c">
Option C
</ListboxOption>
<ListboxOption value="d">Option D</ListboxOption> <ListboxOption value="d">Option D</ListboxOption>
</ListboxOptions> </ListboxOptions>
</Listbox> </Listbox>
@@ -3090,18 +2978,10 @@ describe('Keyboard interactions', () => {
<Listbox v-model="value"> <Listbox v-model="value">
<ListboxButton>Trigger</ListboxButton> <ListboxButton>Trigger</ListboxButton>
<ListboxOptions> <ListboxOptions>
<ListboxOption disabled value="a"> <ListboxOption disabled value="a"> Option A </ListboxOption>
Option A <ListboxOption disabled value="b"> Option B </ListboxOption>
</ListboxOption> <ListboxOption disabled value="c"> Option C </ListboxOption>
<ListboxOption disabled value="b"> <ListboxOption disabled value="d"> Option D </ListboxOption>
Option B
</ListboxOption>
<ListboxOption disabled value="c">
Option C
</ListboxOption>
<ListboxOption disabled value="d">
Option D
</ListboxOption>
</ListboxOptions> </ListboxOptions>
</Listbox> </Listbox>
`, `,
@@ -3252,9 +3132,7 @@ describe('Keyboard interactions', () => {
<ListboxButton>Trigger</ListboxButton> <ListboxButton>Trigger</ListboxButton>
<ListboxOptions> <ListboxOptions>
<ListboxOption value="alice">alice</ListboxOption> <ListboxOption value="alice">alice</ListboxOption>
<ListboxOption disabled value="bob"> <ListboxOption disabled value="bob"> bob </ListboxOption>
bob
</ListboxOption>
<ListboxOption value="charlie">charlie</ListboxOption> <ListboxOption value="charlie">charlie</ListboxOption>
</ListboxOptions> </ListboxOptions>
</Listbox> </Listbox>
@@ -3453,7 +3331,7 @@ describe('Mouse interactions', () => {
// Verify we have listbox options // Verify we have listbox options
let options = getListboxOptions() let options = getListboxOptions()
expect(options).toHaveLength(3) expect(options).toHaveLength(3)
options.forEach(option => assertListboxOption(option)) options.forEach((option) => assertListboxOption(option))
}) })
) )
@@ -3845,9 +3723,7 @@ describe('Mouse interactions', () => {
<ListboxButton>Trigger</ListboxButton> <ListboxButton>Trigger</ListboxButton>
<ListboxOptions> <ListboxOptions>
<ListboxOption value="alice">alice</ListboxOption> <ListboxOption value="alice">alice</ListboxOption>
<ListboxOption disabled value="bob"> <ListboxOption disabled value="bob"> bob </ListboxOption>
bob
</ListboxOption>
<ListboxOption value="charlie">charlie</ListboxOption> <ListboxOption value="charlie">charlie</ListboxOption>
</ListboxOptions> </ListboxOptions>
</Listbox> </Listbox>
@@ -3874,9 +3750,7 @@ describe('Mouse interactions', () => {
<ListboxButton>Trigger</ListboxButton> <ListboxButton>Trigger</ListboxButton>
<ListboxOptions> <ListboxOptions>
<ListboxOption value="alice">alice</ListboxOption> <ListboxOption value="alice">alice</ListboxOption>
<ListboxOption disabled value="bob"> <ListboxOption disabled value="bob"> bob </ListboxOption>
bob
</ListboxOption>
<ListboxOption value="charlie">charlie</ListboxOption> <ListboxOption value="charlie">charlie</ListboxOption>
</ListboxOptions> </ListboxOptions>
</Listbox> </Listbox>
@@ -3951,9 +3825,7 @@ describe('Mouse interactions', () => {
<ListboxButton>Trigger</ListboxButton> <ListboxButton>Trigger</ListboxButton>
<ListboxOptions> <ListboxOptions>
<ListboxOption value="alice">alice</ListboxOption> <ListboxOption value="alice">alice</ListboxOption>
<ListboxOption disabled value="bob"> <ListboxOption disabled value="bob"> bob </ListboxOption>
bob
</ListboxOption>
<ListboxOption value="charlie">charlie</ListboxOption> <ListboxOption value="charlie">charlie</ListboxOption>
</ListboxOptions> </ListboxOptions>
</Listbox> </Listbox>
@@ -4031,9 +3903,7 @@ describe('Mouse interactions', () => {
<ListboxButton>Trigger</ListboxButton> <ListboxButton>Trigger</ListboxButton>
<ListboxOptions> <ListboxOptions>
<ListboxOption value="alice">alice</ListboxOption> <ListboxOption value="alice">alice</ListboxOption>
<ListboxOption disabled value="bob"> <ListboxOption disabled value="bob"> bob </ListboxOption>
bob
</ListboxOption>
<ListboxOption value="charlie">charlie</ListboxOption> <ListboxOption value="charlie">charlie</ListboxOption>
</ListboxOptions> </ListboxOptions>
</Listbox> </Listbox>
@@ -4111,9 +3981,7 @@ describe('Mouse interactions', () => {
<ListboxButton>Trigger</ListboxButton> <ListboxButton>Trigger</ListboxButton>
<ListboxOptions> <ListboxOptions>
<ListboxOption value="alice">alice</ListboxOption> <ListboxOption value="alice">alice</ListboxOption>
<ListboxOption disabled value="bob"> <ListboxOption disabled value="bob"> bob </ListboxOption>
bob
</ListboxOption>
<ListboxOption value="charlie">charlie</ListboxOption> <ListboxOption value="charlie">charlie</ListboxOption>
</ListboxOptions> </ListboxOptions>
</Listbox> </Listbox>
@@ -130,8 +130,8 @@ export let Listbox = defineComponent({
{ {
resolveItems: () => options.value, resolveItems: () => options.value,
resolveActiveIndex: () => activeOptionIndex.value, resolveActiveIndex: () => activeOptionIndex.value,
resolveId: option => option.id, resolveId: (option) => option.id,
resolveDisabled: option => option.dataRef.disabled, resolveDisabled: (option) => option.dataRef.disabled,
} }
) )
@@ -153,7 +153,7 @@ export let Listbox = defineComponent({
: options.value : options.value
let matchingOption = reOrderedOptions.find( let matchingOption = reOrderedOptions.find(
option => (option) =>
option.dataRef.textValue.startsWith(searchQuery.value) && !option.dataRef.disabled option.dataRef.textValue.startsWith(searchQuery.value) && !option.dataRef.disabled
) )
@@ -186,7 +186,7 @@ export let Listbox = defineComponent({
let nextOptions = options.value.slice() let nextOptions = options.value.slice()
let currentActiveOption = let currentActiveOption =
activeOptionIndex.value !== null ? nextOptions[activeOptionIndex.value] : null activeOptionIndex.value !== null ? nextOptions[activeOptionIndex.value] : null
let idx = nextOptions.findIndex(a => a.id === id) let idx = nextOptions.findIndex((a) => a.id === id)
if (idx !== -1) nextOptions.splice(idx, 1) if (idx !== -1) nextOptions.splice(idx, 1)
options.value = nextOptions options.value = nextOptions
activeOptionIndex.value = (() => { activeOptionIndex.value = (() => {
@@ -204,7 +204,7 @@ export let Listbox = defineComponent({
}, },
} }
useWindowEvent('mousedown', event => { useWindowEvent('mousedown', (event) => {
let target = event.target as HTMLElement let target = event.target as HTMLElement
let active = document.activeElement let active = document.activeElement
@@ -529,10 +529,7 @@ export let ListboxOption = defineComponent({
textValue: '', textValue: '',
}) })
onMounted(() => { onMounted(() => {
let textValue = document let textValue = document.getElementById(id)?.textContent?.toLowerCase().trim()
.getElementById(id)
?.textContent?.toLowerCase()
.trim()
if (textValue !== undefined) dataRef.value.textValue = textValue if (textValue !== undefined) dataRef.value.textValue = textValue
}) })
@@ -1,4 +1,12 @@
import { defineComponent, h, nextTick, reactive, ref, watch } from 'vue' import {
ComponentOptionsWithoutProps,
defineComponent,
h,
nextTick,
reactive,
ref,
watch,
} from 'vue'
import { render } from '../../test-utils/vue-testing-library' import { render } from '../../test-utils/vue-testing-library'
import { Menu, MenuButton, MenuItems, MenuItem } from './menu' import { Menu, MenuButton, MenuItems, MenuItem } from './menu'
import { TransitionChild } from '../transitions/transition' import { TransitionChild } from '../transitions/transition'
@@ -44,7 +52,7 @@ beforeAll(() => {
afterAll(() => jest.restoreAllMocks()) afterAll(() => jest.restoreAllMocks())
function nextFrame() { function nextFrame() {
return new Promise(resolve => { return new Promise<void>((resolve) => {
requestAnimationFrame(() => { requestAnimationFrame(() => {
requestAnimationFrame(() => { requestAnimationFrame(() => {
resolve() resolve()
@@ -53,7 +61,7 @@ function nextFrame() {
}) })
} }
function renderTemplate(input: string | Partial<Parameters<typeof defineComponent>[0]>) { function renderTemplate(input: string | ComponentOptionsWithoutProps) {
let defaultComponents = { Menu, MenuButton, MenuItems, MenuItem } let defaultComponents = { Menu, MenuButton, MenuItems, MenuItem }
if (typeof input === 'string') { if (typeof input === 'string') {
@@ -395,7 +403,7 @@ describe('Rendering', () => {
`, `,
setup: () => ({ setup: () => ({
CustomButton: defineComponent({ CustomButton: defineComponent({
setup: props => () => h('button', { ...props }), setup: (props) => () => h('button', { ...props }),
}), }),
}), }),
}) })
@@ -433,7 +441,7 @@ describe('Rendering', () => {
`, `,
setup: () => ({ setup: () => ({
CustomButton: defineComponent({ CustomButton: defineComponent({
setup: props => () => h('div', props), setup: (props) => () => h('div', props),
}), }),
}), }),
}) })
@@ -810,7 +818,7 @@ describe('Rendering composition', () => {
// Verify items are buttons now // Verify items are buttons now
let items = getMenuItems() let items = getMenuItems()
items.forEach(item => items.forEach((item) =>
assertMenuItem(item, { tag: 'button', attributes: { 'data-my-custom-button': 'true' } }) assertMenuItem(item, { tag: 'button', attributes: { 'data-my-custom-button': 'true' } })
) )
}) })
@@ -853,11 +861,11 @@ describe('Rendering composition', () => {
expect.hasAssertions() expect.hasAssertions()
document.querySelectorAll('.outer').forEach(element => { document.querySelectorAll('.outer').forEach((element) => {
expect(element).not.toHaveAttribute('role', 'none') expect(element).not.toHaveAttribute('role', 'none')
}) })
document.querySelectorAll('.inner').forEach(element => { document.querySelectorAll('.inner').forEach((element) => {
expect(element).toHaveAttribute('role', 'none') expect(element).toHaveAttribute('role', 'none')
}) })
}) })
@@ -1069,7 +1077,7 @@ describe('Keyboard interactions', () => {
// Verify we have menu items // Verify we have menu items
let items = getMenuItems() let items = getMenuItems()
expect(items).toHaveLength(3) expect(items).toHaveLength(3)
items.forEach(item => assertMenuItem(item)) items.forEach((item) => assertMenuItem(item))
// Verify that the first menu item is active // Verify that the first menu item is active
assertMenuLinkedWithMenuItem(items[0]) assertMenuLinkedWithMenuItem(items[0])
@@ -1398,7 +1406,7 @@ describe('Keyboard interactions', () => {
// Verify we have menu items // Verify we have menu items
let items = getMenuItems() let items = getMenuItems()
expect(items).toHaveLength(3) expect(items).toHaveLength(3)
items.forEach(item => assertMenuItem(item)) items.forEach((item) => assertMenuItem(item))
assertMenuLinkedWithMenuItem(items[0]) assertMenuLinkedWithMenuItem(items[0])
}) })
@@ -1704,7 +1712,7 @@ describe('Keyboard interactions', () => {
// Verify we have menu items // Verify we have menu items
let items = getMenuItems() let items = getMenuItems()
expect(items).toHaveLength(3) expect(items).toHaveLength(3)
items.forEach(item => assertMenuItem(item)) items.forEach((item) => assertMenuItem(item))
assertMenuLinkedWithMenuItem(items[0]) assertMenuLinkedWithMenuItem(items[0])
// Try to tab // Try to tab
@@ -1750,7 +1758,7 @@ describe('Keyboard interactions', () => {
// Verify we have menu items // Verify we have menu items
let items = getMenuItems() let items = getMenuItems()
expect(items).toHaveLength(3) expect(items).toHaveLength(3)
items.forEach(item => assertMenuItem(item)) items.forEach((item) => assertMenuItem(item))
assertMenuLinkedWithMenuItem(items[0]) assertMenuLinkedWithMenuItem(items[0])
// Try to Shift+Tab // Try to Shift+Tab
@@ -1798,7 +1806,7 @@ describe('Keyboard interactions', () => {
// Verify we have menu items // Verify we have menu items
let items = getMenuItems() let items = getMenuItems()
expect(items).toHaveLength(3) expect(items).toHaveLength(3)
items.forEach(item => assertMenuItem(item)) items.forEach((item) => assertMenuItem(item))
// Verify that the first menu item is active // Verify that the first menu item is active
assertMenuLinkedWithMenuItem(items[0]) assertMenuLinkedWithMenuItem(items[0])
@@ -1883,7 +1891,7 @@ describe('Keyboard interactions', () => {
// Verify we have menu items // Verify we have menu items
let items = getMenuItems() let items = getMenuItems()
expect(items).toHaveLength(3) expect(items).toHaveLength(3)
items.forEach(item => assertMenuItem(item)) items.forEach((item) => assertMenuItem(item))
assertMenuLinkedWithMenuItem(items[0]) assertMenuLinkedWithMenuItem(items[0])
// We should be able to go down once // We should be able to go down once
@@ -1926,7 +1934,7 @@ describe('Keyboard interactions', () => {
// Verify we have menu items // Verify we have menu items
let items = getMenuItems() let items = getMenuItems()
expect(items).toHaveLength(3) expect(items).toHaveLength(3)
items.forEach(item => assertMenuItem(item)) items.forEach((item) => assertMenuItem(item))
assertMenuLinkedWithMenuItem(items[1]) assertMenuLinkedWithMenuItem(items[1])
// We should be able to go down once // We should be able to go down once
@@ -1961,7 +1969,7 @@ describe('Keyboard interactions', () => {
// Verify we have menu items // Verify we have menu items
let items = getMenuItems() let items = getMenuItems()
expect(items).toHaveLength(3) expect(items).toHaveLength(3)
items.forEach(item => assertMenuItem(item)) items.forEach((item) => assertMenuItem(item))
assertMenuLinkedWithMenuItem(items[2]) assertMenuLinkedWithMenuItem(items[2])
}) })
}) })
@@ -2002,7 +2010,7 @@ describe('Keyboard interactions', () => {
// Verify we have menu items // Verify we have menu items
let items = getMenuItems() let items = getMenuItems()
expect(items).toHaveLength(3) expect(items).toHaveLength(3)
items.forEach(item => assertMenuItem(item)) items.forEach((item) => assertMenuItem(item))
// ! ALERT: The LAST item should now be active // ! ALERT: The LAST item should now be active
assertMenuLinkedWithMenuItem(items[2]) assertMenuLinkedWithMenuItem(items[2])
@@ -2055,7 +2063,7 @@ describe('Keyboard interactions', () => {
// Verify we have menu items // Verify we have menu items
let items = getMenuItems() let items = getMenuItems()
expect(items).toHaveLength(3) expect(items).toHaveLength(3)
items.forEach(item => assertMenuItem(item)) items.forEach((item) => assertMenuItem(item))
assertMenuLinkedWithMenuItem(items[0]) assertMenuLinkedWithMenuItem(items[0])
}) })
@@ -2086,7 +2094,7 @@ describe('Keyboard interactions', () => {
// Verify we have menu items // Verify we have menu items
let items = getMenuItems() let items = getMenuItems()
expect(items).toHaveLength(3) expect(items).toHaveLength(3)
items.forEach(item => assertMenuItem(item)) items.forEach((item) => assertMenuItem(item))
assertMenuLinkedWithMenuItem(items[2]) assertMenuLinkedWithMenuItem(items[2])
// We should not be able to go up (because those are disabled) // We should not be able to go up (because those are disabled)
@@ -2133,7 +2141,7 @@ describe('Keyboard interactions', () => {
// Verify we have menu items // Verify we have menu items
let items = getMenuItems() let items = getMenuItems()
expect(items).toHaveLength(3) expect(items).toHaveLength(3)
items.forEach(item => assertMenuItem(item)) items.forEach((item) => assertMenuItem(item))
assertMenuLinkedWithMenuItem(items[2]) assertMenuLinkedWithMenuItem(items[2])
// We should be able to go down once // We should be able to go down once
@@ -2820,7 +2828,7 @@ describe('Mouse interactions', () => {
// Verify we have menu items // Verify we have menu items
let items = getMenuItems() let items = getMenuItems()
expect(items).toHaveLength(3) expect(items).toHaveLength(3)
items.forEach(item => assertMenuItem(item)) items.forEach((item) => assertMenuItem(item))
}) })
it( it(
@@ -96,8 +96,8 @@ export let Menu = defineComponent({
{ {
resolveItems: () => items.value, resolveItems: () => items.value,
resolveActiveIndex: () => activeItemIndex.value, resolveActiveIndex: () => activeItemIndex.value,
resolveId: item => item.id, resolveId: (item) => item.id,
resolveDisabled: item => item.dataRef.disabled, resolveDisabled: (item) => item.dataRef.disabled,
} }
) )
@@ -116,7 +116,7 @@ export let Menu = defineComponent({
: items.value : items.value
let matchingItem = reOrderedItems.find( let matchingItem = reOrderedItems.find(
item => item.dataRef.textValue.startsWith(searchQuery.value) && !item.dataRef.disabled (item) => item.dataRef.textValue.startsWith(searchQuery.value) && !item.dataRef.disabled
) )
let matchIdx = matchingItem ? items.value.indexOf(matchingItem) : -1 let matchIdx = matchingItem ? items.value.indexOf(matchingItem) : -1
@@ -144,7 +144,7 @@ export let Menu = defineComponent({
let nextItems = items.value.slice() let nextItems = items.value.slice()
let currentActiveItem = let currentActiveItem =
activeItemIndex.value !== null ? nextItems[activeItemIndex.value] : null activeItemIndex.value !== null ? nextItems[activeItemIndex.value] : null
let idx = nextItems.findIndex(a => a.id === id) let idx = nextItems.findIndex((a) => a.id === id)
if (idx !== -1) nextItems.splice(idx, 1) if (idx !== -1) nextItems.splice(idx, 1)
items.value = nextItems items.value = nextItems
activeItemIndex.value = (() => { activeItemIndex.value = (() => {
@@ -158,7 +158,7 @@ export let Menu = defineComponent({
}, },
} }
useWindowEvent('mousedown', event => { useWindowEvent('mousedown', (event) => {
let target = event.target as HTMLElement let target = event.target as HTMLElement
let active = document.activeElement let active = document.activeElement
@@ -452,10 +452,7 @@ export let MenuItem = defineComponent({
let dataRef = ref<MenuItemDataRef['value']>({ disabled: props.disabled, textValue: '' }) let dataRef = ref<MenuItemDataRef['value']>({ disabled: props.disabled, textValue: '' })
onMounted(() => { onMounted(() => {
let textValue = document let textValue = document.getElementById(id)?.textContent?.toLowerCase().trim()
.getElementById(id)
?.textContent?.toLowerCase()
.trim()
if (textValue !== undefined) dataRef.value.textValue = textValue if (textValue !== undefined) dataRef.value.textValue = textValue
}) })
@@ -1,4 +1,4 @@
import { defineComponent, nextTick, ref, watch, h } from 'vue' import { defineComponent, nextTick, ref, watch, h, ComponentOptionsWithoutProps } from 'vue'
import { render } from '../../test-utils/vue-testing-library' import { render } from '../../test-utils/vue-testing-library'
import { Popover, PopoverGroup, PopoverButton, PopoverPanel, PopoverOverlay } from './popover' import { Popover, PopoverGroup, PopoverButton, PopoverPanel, PopoverOverlay } from './popover'
@@ -28,7 +28,7 @@ beforeAll(() => {
afterAll(() => jest.restoreAllMocks()) afterAll(() => jest.restoreAllMocks())
function renderTemplate(input: string | Partial<Parameters<typeof defineComponent>[0]>) { function renderTemplate(input: string | ComponentOptionsWithoutProps) {
let defaultComponents = { let defaultComponents = {
Popover, Popover,
PopoverGroup, PopoverGroup,
@@ -345,9 +345,7 @@ describe('Rendering', () => {
renderTemplate( renderTemplate(
html` html`
<Popover> <Popover>
<PopoverButton type="submit"> <PopoverButton type="submit"> Trigger </PopoverButton>
Trigger
</PopoverButton>
</Popover> </Popover>
` `
) )
@@ -361,14 +359,12 @@ describe('Rendering', () => {
renderTemplate({ renderTemplate({
template: html` template: html`
<Popover> <Popover>
<PopoverButton :as="CustomButton"> <PopoverButton :as="CustomButton"> Trigger </PopoverButton>
Trigger
</PopoverButton>
</Popover> </Popover>
`, `,
setup: () => ({ setup: () => ({
CustomButton: defineComponent({ CustomButton: defineComponent({
setup: props => () => h('button', { ...props }), setup: (props) => () => h('button', { ...props }),
}), }),
}), }),
}) })
@@ -383,9 +379,7 @@ describe('Rendering', () => {
renderTemplate( renderTemplate(
html` html`
<Popover> <Popover>
<PopoverButton as="div"> <PopoverButton as="div"> Trigger </PopoverButton>
Trigger
</PopoverButton>
</Popover> </Popover>
` `
) )
@@ -399,14 +393,12 @@ describe('Rendering', () => {
renderTemplate({ renderTemplate({
template: html` template: html`
<Popover> <Popover>
<PopoverButton :as="CustomButton"> <PopoverButton :as="CustomButton"> Trigger </PopoverButton>
Trigger
</PopoverButton>
</Popover> </Popover>
`, `,
setup: () => ({ setup: () => ({
CustomButton: defineComponent({ CustomButton: defineComponent({
setup: props => () => h('div', props), setup: (props) => () => h('div', props),
}), }),
}), }),
}) })
@@ -569,9 +561,7 @@ describe('Rendering', () => {
<Popover> <Popover>
<PopoverButton>Trigger</PopoverButton> <PopoverButton>Trigger</PopoverButton>
<PopoverPanel focus> <PopoverPanel focus>
<a href="/" style="display:none"> <a href="/" style="display:none"> Link 1 </a>
Link 1
</a>
<a href="/">Link 2</a> <a href="/">Link 2</a>
</PopoverPanel> </PopoverPanel>
</Popover> </Popover>
@@ -757,9 +747,7 @@ describe('Composition', () => {
<Popover> <Popover>
<PopoverButton>Trigger</PopoverButton> <PopoverButton>Trigger</PopoverButton>
<OpenClosedWrite :open="true"> <OpenClosedWrite :open="true">
<PopoverPanel v-slot="data"> <PopoverPanel v-slot="data"> {{JSON.stringify(data)}} </PopoverPanel>
{{JSON.stringify(data)}}
</PopoverPanel>
</OpenClosedWrite> </OpenClosedWrite>
</Popover> </Popover>
`, `,
@@ -787,9 +775,7 @@ describe('Composition', () => {
<Popover> <Popover>
<PopoverButton>Trigger</PopoverButton> <PopoverButton>Trigger</PopoverButton>
<OpenClosedWrite :open="false"> <OpenClosedWrite :open="false">
<PopoverPanel v-slot="data"> <PopoverPanel v-slot="data"> {{JSON.stringify(data)}} </PopoverPanel>
{{JSON.stringify(data)}}
</PopoverPanel>
</OpenClosedWrite> </OpenClosedWrite>
</Popover> </Popover>
`, `,
@@ -528,7 +528,7 @@ export let PopoverPanel = defineComponent({
let nextElements = elements let nextElements = elements
.splice(buttonIdx + 1) // Elements after button .splice(buttonIdx + 1) // Elements after button
.filter(element => !dom(api.panel)?.contains(element)) // Ignore items in panel .filter((element) => !dom(api.panel)?.contains(element)) // Ignore items in panel
// Try to focus the next element, however it could fail if we are in a // Try to focus the next element, however it could fail if we are in a
// Portal that happens to be the very last one in the DOM. In that // Portal that happens to be the very last one in the DOM. In that
@@ -624,7 +624,7 @@ export let PopoverGroup = defineComponent({
if (dom(groupRef)?.contains(element)) return true if (dom(groupRef)?.contains(element)) return true
// Check if the focus is in one of the button or panel elements. This is important in case you are rendering inside a Portal. // Check if the focus is in one of the button or panel elements. This is important in case you are rendering inside a Portal.
return popovers.value.some(bag => { return popovers.value.some((bag) => {
return ( return (
document.getElementById(bag.buttonId)?.contains(element) || document.getElementById(bag.buttonId)?.contains(element) ||
document.getElementById(bag.panelId)?.contains(element) document.getElementById(bag.panelId)?.contains(element)
@@ -1,4 +1,4 @@
import { defineComponent, ref, nextTick } from 'vue' import { defineComponent, ref, nextTick, ComponentOptionsWithoutProps } from 'vue'
import { render } from '../../test-utils/vue-testing-library' import { render } from '../../test-utils/vue-testing-library'
import { Portal, PortalGroup } from './portal' import { Portal, PortalGroup } from './portal'
@@ -22,7 +22,7 @@ beforeAll(() => {
afterAll(() => jest.restoreAllMocks()) afterAll(() => jest.restoreAllMocks())
function renderTemplate(input: string | Partial<Parameters<typeof defineComponent>[0]>) { function renderTemplate(input: string | ComponentOptionsWithoutProps) {
let defaultComponents = { Portal, PortalGroup } let defaultComponents = { Portal, PortalGroup }
if (typeof input === 'string') { if (typeof input === 'string') {
@@ -108,12 +108,8 @@ it('should cleanup the Portal root when the last Portal is unmounted', async ()
renderTemplate({ renderTemplate({
template: html` template: html`
<main id="parent"> <main id="parent">
<button id="a" @click="toggleA"> <button id="a" @click="toggleA">Toggle A</button>
Toggle A <button id="b" @click="toggleB">Toggle B</button>
</button>
<button id="b" @click="toggleB">
Toggle B
</button>
<Portal v-if="renderA"> <Portal v-if="renderA">
<p id="content1">Contents 1 ...</p> <p id="content1">Contents 1 ...</p>
@@ -182,19 +178,11 @@ it('should be possible to render multiple portals at the same time', async () =>
renderTemplate({ renderTemplate({
template: html` template: html`
<main id="parent"> <main id="parent">
<button id="a" @click="toggleA"> <button id="a" @click="toggleA">Toggle A</button>
Toggle A <button id="b" @click="toggleB">Toggle B</button>
</button> <button id="c" @click="toggleC">Toggle C</button>
<button id="b" @click="toggleB">
Toggle B
</button>
<button id="c" @click="toggleC">
Toggle C
</button>
<button id="double" @click="toggleAB"> <button id="double" @click="toggleAB">Toggle A & B</button>
Toggle A & B
</button>
<Portal v-if="renderA"> <Portal v-if="renderA">
<p id="content1">Contents 1 ...</p> <p id="content1">Contents 1 ...</p>
@@ -269,12 +257,8 @@ it('should be possible to tamper with the modal root and restore correctly', asy
renderTemplate({ renderTemplate({
template: html` template: html`
<main id="parent"> <main id="parent">
<button id="a" @click="toggleA"> <button id="a" @click="toggleA">Toggle A</button>
Toggle A <button id="b" @click="toggleB">Toggle B</button>
</button>
<button id="b" @click="toggleB">
Toggle B
</button>
<Portal v-if="renderA"> <Portal v-if="renderA">
<p id="content1">Contents 1 ...</p> <p id="content1">Contents 1 ...</p>
@@ -325,9 +309,7 @@ it('should be possible to force the Portal into a specific element using PortalG
renderTemplate({ renderTemplate({
template: html` template: html`
<main> <main>
<aside ref="container" id="group-1"> <aside ref="container" id="group-1">A</aside>
A
</aside>
<PortalGroup :target="container"> <PortalGroup :target="container">
<section id="group-2"> <section id="group-2">
@@ -346,6 +328,6 @@ it('should be possible to force the Portal into a specific element using PortalG
await new Promise<void>(nextTick) await new Promise<void>(nextTick)
expect(document.body.innerHTML).toMatchInlineSnapshot( expect(document.body.innerHTML).toMatchInlineSnapshot(
`"<div><div><div data-v-app=\\"\\"><main><aside id=\\"group-1\\"> A <div>Next to A</div></aside><section id=\\"group-2\\"><span>B</span></section><!--teleport start--><!--teleport end--></main></div></div></div>"` `"<div><div><div data-v-app=\\"\\"><main><aside id=\\"group-1\\">A<div>Next to A</div></aside><section id=\\"group-2\\"><span>B</span></section><!--teleport start--><!--teleport end--></main></div></div></div>"`
) )
}) })
@@ -1,4 +1,4 @@
import { defineComponent, nextTick, ref, watch, reactive } from 'vue' import { defineComponent, nextTick, ref, watch, reactive, ComponentOptionsWithoutProps } from 'vue'
import { render } from '../../test-utils/vue-testing-library' import { render } from '../../test-utils/vue-testing-library'
import { RadioGroup, RadioGroupOption, RadioGroupLabel, RadioGroupDescription } from './radio-group' import { RadioGroup, RadioGroupOption, RadioGroupLabel, RadioGroupDescription } from './radio-group'
@@ -25,7 +25,7 @@ beforeAll(() => {
afterAll(() => jest.restoreAllMocks()) afterAll(() => jest.restoreAllMocks())
function nextFrame() { function nextFrame() {
return new Promise(resolve => { return new Promise<void>((resolve) => {
requestAnimationFrame(() => { requestAnimationFrame(() => {
requestAnimationFrame(() => { requestAnimationFrame(() => {
resolve() resolve()
@@ -34,7 +34,7 @@ function nextFrame() {
}) })
} }
function renderTemplate(input: string | Partial<Parameters<typeof defineComponent>[0]>) { function renderTemplate(input: string | ComponentOptionsWithoutProps) {
let defaultComponents = { RadioGroup, RadioGroupOption, RadioGroupLabel, RadioGroupDescription } let defaultComponents = { RadioGroup, RadioGroupOption, RadioGroupLabel, RadioGroupDescription }
if (typeof input === 'string') { if (typeof input === 'string') {
@@ -86,9 +86,7 @@ describe('Safe guards', () => {
it('should be possible to render a RadioGroup without options and without crashing', () => { it('should be possible to render a RadioGroup without options and without crashing', () => {
renderTemplate({ renderTemplate({
template: html` template: html` <RadioGroup v-model="deliveryMethod" /> `,
<RadioGroup v-model="deliveryMethod" />
`,
setup() { setup() {
let deliveryMethod = ref(undefined) let deliveryMethod = ref(undefined)
return { deliveryMethod } return { deliveryMethod }
@@ -99,19 +99,19 @@ export let RadioGroup = defineComponent({
value, value,
disabled: computed(() => props.disabled), disabled: computed(() => props.disabled),
firstOption: computed(() => firstOption: computed(() =>
options.value.find(option => { options.value.find((option) => {
if (option.propsRef.disabled) return false if (option.propsRef.disabled) return false
return true return true
}) })
), ),
containsCheckedOption: computed(() => containsCheckedOption: computed(() =>
options.value.some(option => toRaw(option.propsRef.value) === toRaw(props.modelValue)) options.value.some((option) => toRaw(option.propsRef.value) === toRaw(props.modelValue))
), ),
change(nextValue: unknown) { change(nextValue: unknown) {
if (props.disabled) return false if (props.disabled) return false
if (value.value === nextValue) return false if (value.value === nextValue) return false
let nextOption = options.value.find( let nextOption = options.value.find(
option => toRaw(option.propsRef.value) === toRaw(nextValue) (option) => toRaw(option.propsRef.value) === toRaw(nextValue)
)?.propsRef )?.propsRef
if (nextOption?.disabled) return false if (nextOption?.disabled) return false
emit('update:modelValue', nextValue) emit('update:modelValue', nextValue)
@@ -129,7 +129,7 @@ export let RadioGroup = defineComponent({
options.value.sort((a, z) => orderMap[a.id] - orderMap[z.id]) options.value.sort((a, z) => orderMap[a.id] - orderMap[z.id])
}, },
unregisterOption(id: Option['id']) { unregisterOption(id: Option['id']) {
let idx = options.value.findIndex(radio => radio.id === id) let idx = options.value.findIndex((radio) => radio.id === id)
if (idx === -1) return if (idx === -1) return
options.value.splice(idx, 1) options.value.splice(idx, 1)
}, },
@@ -155,8 +155,8 @@ export let RadioGroup = defineComponent({
if (!radioGroupRef.value.contains(event.target as HTMLElement)) return if (!radioGroupRef.value.contains(event.target as HTMLElement)) return
let all = options.value let all = options.value
.filter(option => option.propsRef.disabled === false) .filter((option) => option.propsRef.disabled === false)
.map(radio => radio.element) as HTMLElement[] .map((radio) => radio.element) as HTMLElement[]
switch (event.key) { switch (event.key) {
case Keys.ArrowLeft: case Keys.ArrowLeft:
@@ -169,7 +169,7 @@ export let RadioGroup = defineComponent({
if (result === FocusResult.Success) { if (result === FocusResult.Success) {
let activeOption = options.value.find( let activeOption = options.value.find(
option => option.element === document.activeElement (option) => option.element === document.activeElement
) )
if (activeOption) api.change(activeOption.propsRef.value) if (activeOption) api.change(activeOption.propsRef.value)
} }
@@ -186,7 +186,7 @@ export let RadioGroup = defineComponent({
if (result === FocusResult.Success) { if (result === FocusResult.Success) {
let activeOption = options.value.find( let activeOption = options.value.find(
option => option.element === document.activeElement (option) => option.element === document.activeElement
) )
if (activeOption) api.change(activeOption.propsRef.value) if (activeOption) api.change(activeOption.propsRef.value)
} }
@@ -199,7 +199,7 @@ export let RadioGroup = defineComponent({
event.stopPropagation() event.stopPropagation()
let activeOption = options.value.find( let activeOption = options.value.find(
option => option.element === document.activeElement (option) => option.element === document.activeElement
) )
if (activeOption) api.change(activeOption.propsRef.value) if (activeOption) api.change(activeOption.propsRef.value)
} }
@@ -1,4 +1,4 @@
import { defineComponent, ref, watch, h } from 'vue' import { defineComponent, ref, watch, h, ComponentOptionsWithoutProps } from 'vue'
import { render } from '../../test-utils/vue-testing-library' import { render } from '../../test-utils/vue-testing-library'
import { Switch, SwitchLabel, SwitchDescription, SwitchGroup } from './switch' import { Switch, SwitchLabel, SwitchDescription, SwitchGroup } from './switch'
@@ -16,7 +16,7 @@ import { suppressConsoleLogs } from '../../test-utils/suppress-console-logs'
jest.mock('../../hooks/use-id') jest.mock('../../hooks/use-id')
function renderTemplate(input: string | Partial<Parameters<typeof defineComponent>[0]>) { function renderTemplate(input: string | ComponentOptionsWithoutProps) {
let defaultComponents = { Switch, SwitchLabel, SwitchDescription, SwitchGroup } let defaultComponents = { Switch, SwitchLabel, SwitchDescription, SwitchGroup }
if (typeof input === 'string') { if (typeof input === 'string') {
@@ -35,9 +35,7 @@ function renderTemplate(input: string | Partial<Parameters<typeof defineComponen
describe('Safe guards', () => { describe('Safe guards', () => {
it('should be possible to render a Switch without crashing', () => { it('should be possible to render a Switch without crashing', () => {
renderTemplate({ renderTemplate({
template: html` template: html` <Switch v-model="checked" /> `,
<Switch v-model="checked" />
`,
setup: () => ({ checked: ref(false) }), setup: () => ({ checked: ref(false) }),
}) })
}) })
@@ -72,9 +70,7 @@ describe('Rendering', () => {
it('should be possible to render an (on) Switch using an `as` prop', () => { it('should be possible to render an (on) Switch using an `as` prop', () => {
renderTemplate({ renderTemplate({
template: html` template: html` <Switch as="span" v-model="checked" /> `,
<Switch as="span" v-model="checked" />
`,
setup: () => ({ checked: ref(true) }), setup: () => ({ checked: ref(true) }),
}) })
assertSwitch({ state: SwitchState.On, tag: 'span' }) assertSwitch({ state: SwitchState.On, tag: 'span' })
@@ -82,9 +78,7 @@ describe('Rendering', () => {
it('should be possible to render an (off) Switch using an `as` prop', () => { it('should be possible to render an (off) Switch using an `as` prop', () => {
renderTemplate({ renderTemplate({
template: html` template: html` <Switch as="span" v-model="checked" /> `,
<Switch as="span" v-model="checked" />
`,
setup: () => ({ checked: ref(false) }), setup: () => ({ checked: ref(false) }),
}) })
assertSwitch({ state: SwitchState.Off, tag: 'span' }) assertSwitch({ state: SwitchState.Off, tag: 'span' })
@@ -106,11 +100,7 @@ describe('Rendering', () => {
describe('`type` attribute', () => { describe('`type` attribute', () => {
it('should set the `type` to "button" by default', async () => { it('should set the `type` to "button" by default', async () => {
renderTemplate({ renderTemplate({
template: html` template: html` <Switch v-model="checked"> Trigger </Switch> `,
<Switch v-model="checked">
Trigger
</Switch>
`,
setup: () => ({ checked: ref(false) }), setup: () => ({ checked: ref(false) }),
}) })
@@ -119,11 +109,7 @@ describe('Rendering', () => {
it('should not set the `type` to "button" if it already contains a `type`', async () => { it('should not set the `type` to "button" if it already contains a `type`', async () => {
renderTemplate({ renderTemplate({
template: html` template: html` <Switch v-model="checked" type="submit"> Trigger </Switch> `,
<Switch v-model="checked" type="submit">
Trigger
</Switch>
`,
setup: () => ({ checked: ref(false) }), setup: () => ({ checked: ref(false) }),
}) })
@@ -134,15 +120,11 @@ describe('Rendering', () => {
'should set the `type` to "button" when using the `as` prop which resolves to a "button"', 'should set the `type` to "button" when using the `as` prop which resolves to a "button"',
suppressConsoleLogs(async () => { suppressConsoleLogs(async () => {
renderTemplate({ renderTemplate({
template: html` template: html` <Switch v-model="checked" :as="CustomButton"> Trigger </Switch> `,
<Switch v-model="checked" :as="CustomButton">
Trigger
</Switch>
`,
setup: () => ({ setup: () => ({
checked: ref(false), checked: ref(false),
CustomButton: defineComponent({ CustomButton: defineComponent({
setup: props => () => h('button', { ...props }), setup: (props) => () => h('button', { ...props }),
}), }),
}), }),
}) })
@@ -155,11 +137,7 @@ describe('Rendering', () => {
it('should not set the type if the "as" prop is not a "button"', async () => { it('should not set the type if the "as" prop is not a "button"', async () => {
renderTemplate({ renderTemplate({
template: html` template: html` <Switch v-model="checked" as="div"> Trigger </Switch> `,
<Switch v-model="checked" as="div">
Trigger
</Switch>
`,
setup: () => ({ checked: ref(false) }), setup: () => ({ checked: ref(false) }),
}) })
@@ -170,15 +148,11 @@ describe('Rendering', () => {
'should not set the `type` to "button" when using the `as` prop which resolves to a "div"', 'should not set the `type` to "button" when using the `as` prop which resolves to a "div"',
suppressConsoleLogs(async () => { suppressConsoleLogs(async () => {
renderTemplate({ renderTemplate({
template: html` template: html` <Switch v-model="checked" :as="CustomButton"> Trigger </Switch> `,
<Switch v-model="checked" :as="CustomButton">
Trigger
</Switch>
`,
setup: () => ({ setup: () => ({
checked: ref(false), checked: ref(false),
CustomButton: defineComponent({ CustomButton: defineComponent({
setup: props => () => h('div', props), setup: (props) => () => h('div', props),
}), }),
}), }),
}) })
@@ -213,9 +187,7 @@ describe('Render composition', () => {
template: html` template: html`
<SwitchGroup> <SwitchGroup>
<SwitchLabel>Label B</SwitchLabel> <SwitchLabel>Label B</SwitchLabel>
<Switch v-model="checked"> <Switch v-model="checked"> Label A </Switch>
Label A
</Switch>
</SwitchGroup> </SwitchGroup>
`, `,
setup: () => ({ checked: ref(false) }), setup: () => ({ checked: ref(false) }),
@@ -234,9 +206,7 @@ describe('Render composition', () => {
renderTemplate({ renderTemplate({
template: html` template: html`
<SwitchGroup> <SwitchGroup>
<Switch v-model="checked"> <Switch v-model="checked"> Label A </Switch>
Label A
</Switch>
<SwitchLabel>Label B</SwitchLabel> <SwitchLabel>Label B</SwitchLabel>
</SwitchGroup> </SwitchGroup>
`, `,
@@ -370,9 +340,7 @@ describe('Keyboard interactions', () => {
it('should be possible to toggle the Switch with Space', async () => { it('should be possible to toggle the Switch with Space', async () => {
let handleChange = jest.fn() let handleChange = jest.fn()
renderTemplate({ renderTemplate({
template: html` template: html` <Switch v-model="checked" /> `,
<Switch v-model="checked" />
`,
setup() { setup() {
let checked = ref(false) let checked = ref(false)
watch([checked], () => handleChange(checked.value)) watch([checked], () => handleChange(checked.value))
@@ -404,9 +372,7 @@ describe('Keyboard interactions', () => {
it('should not be possible to use Enter to toggle the Switch', async () => { it('should not be possible to use Enter to toggle the Switch', async () => {
let handleChange = jest.fn() let handleChange = jest.fn()
renderTemplate({ renderTemplate({
template: html` template: html` <Switch v-model="checked" /> `,
<Switch v-model="checked" />
`,
setup() { setup() {
let checked = ref(false) let checked = ref(false)
watch([checked], () => handleChange(checked.value)) watch([checked], () => handleChange(checked.value))
@@ -461,9 +427,7 @@ describe('Mouse interactions', () => {
it('should be possible to toggle the Switch with a click', async () => { it('should be possible to toggle the Switch with a click', async () => {
let handleChange = jest.fn() let handleChange = jest.fn()
renderTemplate({ renderTemplate({
template: html` template: html` <Switch v-model="checked" /> `,
<Switch v-model="checked" />
`,
setup() { setup() {
let checked = ref(false) let checked = ref(false)
watch([checked], () => handleChange(checked.value)) watch([checked], () => handleChange(checked.value))
@@ -1,4 +1,4 @@
import { defineComponent, nextTick, ref } from 'vue' import { ComponentOptionsWithoutProps, defineComponent, nextTick, ref } from 'vue'
import { render } from '../../test-utils/vue-testing-library' import { render } from '../../test-utils/vue-testing-library'
import { TabGroup, TabList, Tab, TabPanels, TabPanel } from './tabs' import { TabGroup, TabList, Tab, TabPanels, TabPanel } from './tabs'
import { suppressConsoleLogs } from '../../test-utils/suppress-console-logs' import { suppressConsoleLogs } from '../../test-utils/suppress-console-logs'
@@ -20,7 +20,7 @@ beforeAll(() => {
afterAll(() => jest.restoreAllMocks()) afterAll(() => jest.restoreAllMocks())
function renderTemplate(input: string | Partial<Parameters<typeof defineComponent>[0]>) { function renderTemplate(input: string | ComponentOptionsWithoutProps) {
let defaultComponents = { TabGroup, TabList, Tab, TabPanels, TabPanel } let defaultComponents = { TabGroup, TabList, Tab, TabPanels, TabPanel }
if (typeof input === 'string') { if (typeof input === 'string') {
@@ -102,8 +102,8 @@ export let TabGroup = defineComponent({
if (api.tabs.value.length <= 0) return if (api.tabs.value.length <= 0) return
if (props.selectedIndex === null && selectedIndex.value !== null) return if (props.selectedIndex === null && selectedIndex.value !== null) return
let tabs = api.tabs.value.map(tab => dom(tab)).filter(Boolean) as HTMLElement[] let tabs = api.tabs.value.map((tab) => dom(tab)).filter(Boolean) as HTMLElement[]
let focusableTabs = tabs.filter(tab => !tab.hasAttribute('disabled')) let focusableTabs = tabs.filter((tab) => !tab.hasAttribute('disabled'))
let indexToSet = props.selectedIndex ?? props.defaultIndex let indexToSet = props.selectedIndex ?? props.defaultIndex
@@ -122,7 +122,7 @@ export let TabGroup = defineComponent({
let before = tabs.slice(0, indexToSet) let before = tabs.slice(0, indexToSet)
let after = tabs.slice(indexToSet) let after = tabs.slice(indexToSet)
let next = [...after, ...before].find(tab => focusableTabs.includes(tab)) let next = [...after, ...before].find((tab) => focusableTabs.includes(tab))
if (!next) return if (!next) return
selectedIndex.value = tabs.indexOf(next) selectedIndex.value = tabs.indexOf(next)
@@ -220,7 +220,7 @@ export let Tab = defineComponent({
let selected = computed(() => myIndex.value === api.selectedIndex.value) let selected = computed(() => myIndex.value === api.selectedIndex.value)
function handleKeyDown(event: KeyboardEvent) { function handleKeyDown(event: KeyboardEvent) {
let list = api.tabs.value.map(tab => dom(tab)).filter(Boolean) as HTMLElement[] let list = api.tabs.value.map((tab) => dom(tab)).filter(Boolean) as HTMLElement[]
if (event.key === Keys.Space || event.key === Keys.Enter) { if (event.key === Keys.Space || event.key === Keys.Enter) {
event.preventDefault() event.preventDefault()
@@ -1,4 +1,4 @@
import { defineComponent, ref, onMounted } from 'vue' import { defineComponent, ref, onMounted, ComponentOptionsWithoutProps } from 'vue'
import { render, fireEvent } from '../../test-utils/vue-testing-library' import { render, fireEvent } from '../../test-utils/vue-testing-library'
import { suppressConsoleLogs } from '../../test-utils/suppress-console-logs' import { suppressConsoleLogs } from '../../test-utils/suppress-console-logs'
@@ -11,7 +11,7 @@ jest.mock('../../hooks/use-id')
afterAll(() => jest.restoreAllMocks()) afterAll(() => jest.restoreAllMocks())
function renderTemplate(input: string | Partial<Parameters<typeof defineComponent>[0]>) { function renderTemplate(input: string | ComponentOptionsWithoutProps) {
let defaultComponents = { TransitionRoot, TransitionChild } let defaultComponents = { TransitionRoot, TransitionChild }
if (typeof input === 'string') { if (typeof input === 'string') {
@@ -58,9 +58,7 @@ it('should render without crashing', () => {
it('should be possible to render a Transition without children', () => { it('should be possible to render a Transition without children', () => {
renderTemplate({ renderTemplate({
template: html` template: html` <TransitionRoot :show="true" class="transition" /> `,
<TransitionRoot :show="true" class="transition" />
`,
}) })
expect(document.getElementsByClassName('transition')).not.toBeNull() expect(document.getElementsByClassName('transition')).not.toBeNull()
}) })
@@ -91,12 +89,10 @@ describe('Setup API', () => {
describe('shallow', () => { describe('shallow', () => {
it('should render a div and its children by default', () => { it('should render a div and its children by default', () => {
let { container } = renderTemplate({ let { container } = renderTemplate({
template: html` template: html`<TransitionRoot :show="true">Children</TransitionRoot>`,
<TransitionRoot :show="true">Children</TransitionRoot>
`,
}) })
expect(container.firstChild).toMatchInlineSnapshot(html` expect(container.firstChild).toMatchInlineSnapshot(`
<div> <div>
Children Children
</div> </div>
@@ -106,9 +102,7 @@ describe('Setup API', () => {
it('should passthrough all the props (that we do not use internally)', () => { it('should passthrough all the props (that we do not use internally)', () => {
let { container } = renderTemplate({ let { container } = renderTemplate({
template: html` template: html`
<TransitionRoot :show="true" id="root" class="text-blue-400"> <TransitionRoot :show="true" id="root" class="text-blue-400"> Children </TransitionRoot>
Children
</TransitionRoot>
`, `,
}) })
@@ -124,11 +118,7 @@ describe('Setup API', () => {
it('should render another component if the `as` prop is used and its children by default', () => { it('should render another component if the `as` prop is used and its children by default', () => {
let { container } = renderTemplate({ let { container } = renderTemplate({
template: html` template: html` <TransitionRoot :show="true" as="a"> Children </TransitionRoot> `,
<TransitionRoot :show="true" as="a">
Children
</TransitionRoot>
`,
}) })
expect(container.firstChild).toMatchInlineSnapshot(` expect(container.firstChild).toMatchInlineSnapshot(`
@@ -159,9 +149,7 @@ describe('Setup API', () => {
it('should render nothing when the show prop is false', () => { it('should render nothing when the show prop is false', () => {
let { container } = renderTemplate({ let { container } = renderTemplate({
template: html` template: html` <TransitionRoot :show="false">Children</TransitionRoot> `,
<TransitionRoot :show="false">Children</TransitionRoot>
`,
}) })
expect(container.firstChild).toMatchInlineSnapshot(`<!---->`) expect(container.firstChild).toMatchInlineSnapshot(`<!---->`)
@@ -169,11 +157,7 @@ describe('Setup API', () => {
it('should be possible to change the underlying DOM tag', () => { it('should be possible to change the underlying DOM tag', () => {
let { container } = renderTemplate({ let { container } = renderTemplate({
template: html` template: html` <TransitionRoot :show="true" as="a"> Children </TransitionRoot> `,
<TransitionRoot :show="true" as="a">
Children
</TransitionRoot>
`,
}) })
expect(container.firstChild).toMatchInlineSnapshot(` expect(container.firstChild).toMatchInlineSnapshot(`
@@ -470,9 +454,7 @@ describe('Transitions', () => {
<span>Hello!</span> <span>Hello!</span>
</TransitionRoot> </TransitionRoot>
<button data-testid="toggle" @click="show = !show"> <button data-testid="toggle" @click="show = !show">Toggle</button>
Toggle
</button>
`, `,
setup() { setup() {
let show = ref(false) let show = ref(false)
@@ -525,9 +507,7 @@ describe('Transitions', () => {
<span>Hello!</span> <span>Hello!</span>
</TransitionRoot> </TransitionRoot>
<button data-testid="toggle" @click="show = !show"> <button data-testid="toggle" @click="show = !show">Toggle</button>
Toggle
</button>
`, `,
setup() { setup() {
let show = ref(false) let show = ref(false)
@@ -580,9 +560,7 @@ describe('Transitions', () => {
<span>Hello!</span> <span>Hello!</span>
</TransitionRoot> </TransitionRoot>
<button data-testid="toggle" @click="show = !show"> <button data-testid="toggle" @click="show = !show">Toggle</button>
Toggle
</button>
`, `,
setup() { setup() {
let show = ref(false) let show = ref(false)
@@ -630,9 +608,7 @@ describe('Transitions', () => {
<span>Hello!</span> <span>Hello!</span>
</TransitionRoot> </TransitionRoot>
<button data-testid="toggle" @click="show = !show"> <button data-testid="toggle" @click="show = !show">Toggle</button>
Toggle
</button>
`, `,
setup() { setup() {
let show = ref(false) let show = ref(false)
@@ -687,9 +663,7 @@ describe('Transitions', () => {
<span>Hello!</span> <span>Hello!</span>
</TransitionRoot> </TransitionRoot>
<button data-testid="toggle" @click="show = !show"> <button data-testid="toggle" @click="show = !show">Toggle</button>
Toggle
</button>
`, `,
setup() { setup() {
let show = ref(true) let show = ref(true)
@@ -753,9 +727,7 @@ describe('Transitions', () => {
<span>Hello!</span> <span>Hello!</span>
</TransitionRoot> </TransitionRoot>
<button data-testid="toggle" @click="show = !show"> <button data-testid="toggle" @click="show = !show">Toggle</button>
Toggle
</button>
`, `,
setup() { setup() {
let show = ref(true) let show = ref(true)
@@ -822,9 +794,7 @@ describe('Transitions', () => {
<span>Hello!</span> <span>Hello!</span>
</TransitionRoot> </TransitionRoot>
<button data-testid="toggle" @click="show = !show"> <button data-testid="toggle" @click="show = !show">Toggle</button>
Toggle
</button>
`, `,
setup() { setup() {
let show = ref(false) let show = ref(false)
@@ -918,9 +888,7 @@ describe('Transitions', () => {
<span>Hello!</span> <span>Hello!</span>
</TransitionRoot> </TransitionRoot>
<button data-testid="toggle" @click="show = !show"> <button data-testid="toggle" @click="show = !show">Toggle</button>
Toggle
</button>
`, `,
setup() { setup() {
let show = ref(false) let show = ref(false)
@@ -1021,9 +989,7 @@ describe('Transitions', () => {
</TransitionChild> </TransitionChild>
</TransitionRoot> </TransitionRoot>
<button data-testid="toggle" @click="show = !show"> <button data-testid="toggle" @click="show = !show">Toggle</button>
Toggle
</button>
`, `,
setup() { setup() {
let show = ref(true) let show = ref(true)
@@ -1113,9 +1079,7 @@ describe('Transitions', () => {
</TransitionChild> </TransitionChild>
</TransitionRoot> </TransitionRoot>
<button data-testid="toggle" @click="show = !show"> <button data-testid="toggle" @click="show = !show">Toggle</button>
Toggle
</button>
`, `,
setup() { setup() {
let show = ref(true) let show = ref(true)
@@ -1227,9 +1191,7 @@ describe('Events', () => {
<span>Hello!</span> <span>Hello!</span>
</TransitionRoot> </TransitionRoot>
<button data-testid="toggle" @click="show = !show"> <button data-testid="toggle" @click="show = !show">Toggle</button>
Toggle
</button>
`, `,
setup() { setup() {
let show = ref(false) let show = ref(false)
@@ -31,7 +31,7 @@ import {
type ID = ReturnType<typeof useId> type ID = ReturnType<typeof useId>
function splitClasses(classes: string = '') { function splitClasses(classes: string = '') {
return classes.split(' ').filter(className => className.trim().length > 1) return classes.split(' ').filter((className) => className.trim().length > 1)
} }
interface TransitionContextValues { interface TransitionContextValues {
@@ -292,7 +292,7 @@ export let TransitionChild = defineComponent({
enterFromClasses, enterFromClasses,
enterToClasses, enterToClasses,
enteredClasses, enteredClasses,
reason => { (reason) => {
isTransitioning.value = false isTransitioning.value = false
if (reason === Reason.Finished) emit('afterEnter') if (reason === Reason.Finished) emit('afterEnter')
} }
@@ -303,7 +303,7 @@ export let TransitionChild = defineComponent({
leaveFromClasses, leaveFromClasses,
leaveToClasses, leaveToClasses,
enteredClasses, enteredClasses,
reason => { (reason) => {
isTransitioning.value = false isTransitioning.value = false
if (reason !== Reason.Finished) return if (reason !== Reason.Finished) return
@@ -17,7 +17,7 @@ it('should be possible to transition', async () => {
d.add( d.add(
reportChanges( reportChanges(
() => document.body.innerHTML, () => document.body.innerHTML,
content => { (content) => {
snapshots.push({ snapshots.push({
content, content,
recordedAt: process.hrtime.bigint(), recordedAt: process.hrtime.bigint(),
@@ -26,11 +26,11 @@ it('should be possible to transition', async () => {
) )
) )
await new Promise(resolve => { await new Promise((resolve) => {
transition(element, ['enter'], ['enterFrom'], ['enterTo'], ['entered'], resolve) transition(element, ['enter'], ['enterFrom'], ['enterTo'], ['entered'], resolve)
}) })
await new Promise(resolve => d.nextFrame(resolve)) await new Promise((resolve) => d.nextFrame(resolve))
// Initial render: // Initial render:
expect(snapshots[0].content).toEqual('<div></div>') expect(snapshots[0].content).toEqual('<div></div>')
@@ -61,7 +61,7 @@ it('should wait the correct amount of time to finish a transition', async () =>
d.add( d.add(
reportChanges( reportChanges(
() => document.body.innerHTML, () => document.body.innerHTML,
content => { (content) => {
snapshots.push({ snapshots.push({
content, content,
recordedAt: process.hrtime.bigint(), recordedAt: process.hrtime.bigint(),
@@ -70,11 +70,11 @@ it('should wait the correct amount of time to finish a transition', async () =>
) )
) )
let reason = await new Promise(resolve => { let reason = await new Promise((resolve) => {
transition(element, ['enter'], ['enterFrom'], ['enterTo'], ['entered'], resolve) transition(element, ['enter'], ['enterFrom'], ['enterTo'], ['entered'], resolve)
}) })
await new Promise(resolve => d.nextFrame(resolve)) await new Promise((resolve) => d.nextFrame(resolve))
expect(reason).toBe(Reason.Finished) expect(reason).toBe(Reason.Finished)
// Initial render: // Initial render:
@@ -118,7 +118,7 @@ it('should keep the delay time into account', async () => {
d.add( d.add(
reportChanges( reportChanges(
() => document.body.innerHTML, () => document.body.innerHTML,
content => { (content) => {
snapshots.push({ snapshots.push({
content, content,
recordedAt: process.hrtime.bigint(), recordedAt: process.hrtime.bigint(),
@@ -127,11 +127,11 @@ it('should keep the delay time into account', async () => {
) )
) )
let reason = await new Promise(resolve => { let reason = await new Promise((resolve) => {
transition(element, ['enter'], ['enterFrom'], ['enterTo'], ['entered'], resolve) transition(element, ['enter'], ['enterFrom'], ['enterTo'], ['entered'], resolve)
}) })
await new Promise(resolve => d.nextFrame(resolve)) await new Promise((resolve) => d.nextFrame(resolve))
expect(reason).toBe(Reason.Finished) expect(reason).toBe(Reason.Finished)
let estimatedDuration = Number( let estimatedDuration = Number(
@@ -161,7 +161,7 @@ it('should be possible to cancel a transition at any time', async () => {
d.add( d.add(
reportChanges( reportChanges(
() => document.body.innerHTML, () => document.body.innerHTML,
content => { (content) => {
let recordedAt = process.hrtime.bigint() let recordedAt = process.hrtime.bigint()
let total = snapshots.length let total = snapshots.length
@@ -178,16 +178,16 @@ it('should be possible to cancel a transition at any time', async () => {
expect.assertions(2) expect.assertions(2)
// Setup the transition // Setup the transition
let cancel = transition(element, ['enter'], ['enterFrom'], ['enterTo'], ['entered'], reason => { let cancel = transition(element, ['enter'], ['enterFrom'], ['enterTo'], ['entered'], (reason) => {
expect(reason).toBe(Reason.Cancelled) expect(reason).toBe(Reason.Cancelled)
}) })
// Wait for a bit // Wait for a bit
await new Promise(resolve => setTimeout(resolve, 20)) await new Promise((resolve) => setTimeout(resolve, 20))
// Cancel the transition // Cancel the transition
cancel() cancel()
await new Promise(resolve => d.nextFrame(resolve)) await new Promise((resolve) => d.nextFrame(resolve))
expect(snapshots.map(snapshot => snapshot.content).join('\n')).not.toContain('enterTo') expect(snapshots.map((snapshot) => snapshot.content).join('\n')).not.toContain('enterTo')
}) })
@@ -22,13 +22,13 @@ function waitForTransition(node: HTMLElement, done: (reason: Reason) => void) {
// Safari returns a comma separated list of values, so let's sort them and take the highest value. // Safari returns a comma separated list of values, so let's sort them and take the highest value.
let { transitionDuration, transitionDelay } = getComputedStyle(node) let { transitionDuration, transitionDelay } = getComputedStyle(node)
let [durationMs, delaysMs] = [transitionDuration, transitionDelay].map(value => { let [durationMs, delaysMs] = [transitionDuration, transitionDelay].map((value) => {
let [resolvedValue = 0] = value let [resolvedValue = 0] = value
.split(',') .split(',')
// Remove falseys we can't work with // Remove falseys we can't work with
.filter(Boolean) .filter(Boolean)
// Values are returned as `0.3s` or `75ms` // Values are returned as `0.3s` or `75ms`
.map(v => (v.includes('ms') ? parseFloat(v) : parseFloat(v) * 1000)) .map((v) => (v.includes('ms') ? parseFloat(v) : parseFloat(v) * 1000))
.sort((a, z) => z - a) .sort((a, z) => z - a)
return resolvedValue return resolvedValue
@@ -72,7 +72,7 @@ export function transition(
addClasses(node, ...to) addClasses(node, ...to)
d.add( d.add(
waitForTransition(node, reason => { waitForTransition(node, (reason) => {
removeClasses(node, ...to, ...base) removeClasses(node, ...to, ...base)
addClasses(node, ...entered) addClasses(node, ...entered)
return _done(reason) return _done(reason)
@@ -75,7 +75,7 @@ export function useFocusTrap(
onUnmounted(restore) onUnmounted(restore)
// Handle Tab & Shift+Tab keyboard events // Handle Tab & Shift+Tab keyboard events
useWindowEvent('keydown', event => { useWindowEvent('keydown', (event) => {
if (!enabled.value) return if (!enabled.value) return
if (event.key !== Keys.Tab) return if (event.key !== Keys.Tab) return
if (!document.activeElement) return if (!document.activeElement) return
@@ -99,7 +99,7 @@ export function useFocusTrap(
// Prevent programmatically escaping // Prevent programmatically escaping
useWindowEvent( useWindowEvent(
'focus', 'focus',
event => { (event) => {
if (!enabled.value) return if (!enabled.value) return
if (containers.value.size !== 1) return if (containers.value.size !== 1) return
@@ -1,4 +1,4 @@
import { defineComponent, ref, nextTick } from 'vue' import { defineComponent, ref, nextTick, ComponentOptionsWithoutProps } from 'vue'
import { render } from '../test-utils/vue-testing-library' import { render } from '../test-utils/vue-testing-library'
import { useInertOthers } from './use-inert-others' import { useInertOthers } from './use-inert-others'
@@ -14,7 +14,7 @@ beforeAll(() => {
afterAll(() => jest.restoreAllMocks()) afterAll(() => jest.restoreAllMocks())
function renderTemplate(input: string | Partial<Parameters<typeof defineComponent>[0]>) { function renderTemplate(input: string | ComponentOptionsWithoutProps) {
let defaultComponents = {} let defaultComponents = {}
if (typeof input === 'string') { if (typeof input === 'string') {
@@ -35,16 +35,12 @@ function renderTemplate(input: string | Partial<Parameters<typeof defineComponen
let Before = defineComponent({ let Before = defineComponent({
name: 'Before', name: 'Before',
template: html` template: html` <div>before</div> `,
<div>before</div>
`,
}) })
let After = defineComponent({ let After = defineComponent({
name: 'After', name: 'After',
template: html` template: html` <div>after</div> `,
<div>after</div>
`,
}) })
it('should be possible to inert other elements', async () => { it('should be possible to inert other elements', async () => {
@@ -32,7 +32,7 @@ export function useInertOthers<TElement extends HTMLElement>(
container: Ref<TElement | null>, container: Ref<TElement | null>,
enabled: Ref<boolean> = ref(true) enabled: Ref<boolean> = ref(true)
) { ) {
watchEffect(onInvalidate => { watchEffect((onInvalidate) => {
if (!enabled.value) return if (!enabled.value) return
if (!container.value) return if (!container.value) return
@@ -50,7 +50,7 @@ export function useInertOthers<TElement extends HTMLElement>(
} }
// Collect direct children of the body // Collect direct children of the body
document.querySelectorAll(CHILDREN_SELECTOR).forEach(child => { document.querySelectorAll(CHILDREN_SELECTOR).forEach((child) => {
if (!(child instanceof HTMLElement)) return // Skip non-HTMLElements if (!(child instanceof HTMLElement)) return // Skip non-HTMLElements
// Skip the interactables, and the parents of the interactables // Skip the interactables, and the parents of the interactables
@@ -79,7 +79,7 @@ export function useInertOthers<TElement extends HTMLElement>(
// will become inert as well. // will become inert as well.
if (interactables.size > 0) { if (interactables.size > 0) {
// Collect direct children of the body // Collect direct children of the body
document.querySelectorAll(CHILDREN_SELECTOR).forEach(child => { document.querySelectorAll(CHILDREN_SELECTOR).forEach((child) => {
if (!(child instanceof HTMLElement)) return // Skip non-HTMLElements if (!(child instanceof HTMLElement)) return // Skip non-HTMLElements
// Skip already inert parents // Skip already inert parents
@@ -24,6 +24,7 @@ export function useTreeWalker({
if (enabled !== undefined && !enabled.value) return if (enabled !== undefined && !enabled.value) return
let acceptNode = Object.assign((node: HTMLElement) => accept(node), { acceptNode: accept }) let acceptNode = Object.assign((node: HTMLElement) => accept(node), { acceptNode: accept })
// @ts-expect-error This `false` is a simple small fix for older browsers
let walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT, acceptNode, false) let walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT, acceptNode, false)
while (walker.nextNode()) walk(walker.currentNode as HTMLElement) while (walker.nextNode()) walk(walker.currentNode as HTMLElement)
@@ -7,7 +7,7 @@ export function useWindowEvent<TType extends keyof WindowEventMap>(
) { ) {
if (typeof window === 'undefined') return if (typeof window === 'undefined') return
watchEffect(onInvalidate => { watchEffect((onInvalidate) => {
window.addEventListener(type, listener, options) window.addEventListener(type, listener, options)
onInvalidate(() => { onInvalidate(() => {
@@ -24,7 +24,7 @@ export function useStackContext() {
export function useElemenStack(element: Ref<HTMLElement | null> | null) { export function useElemenStack(element: Ref<HTMLElement | null> | null) {
let notify = useStackContext() let notify = useStackContext()
watchEffect(onInvalidate => { watchEffect((onInvalidate) => {
let domElement = element?.value let domElement = element?.value
if (!domElement) return if (!domElement) return
@@ -1256,7 +1256,7 @@ export function assertLabelValue(element: HTMLElement | null, value: string) {
if (element.hasAttribute('aria-labelledby')) { if (element.hasAttribute('aria-labelledby')) {
let ids = element.getAttribute('aria-labelledby')!.split(' ') let ids = element.getAttribute('aria-labelledby')!.split(' ')
expect(ids.map(id => document.getElementById(id)?.textContent).join(' ')).toEqual(value) expect(ids.map((id) => document.getElementById(id)?.textContent).join(' ')).toEqual(value)
return return
} }
@@ -1612,7 +1612,7 @@ export function assertTabs(
expect(list).toHaveAttribute('aria-orientation', orientation) expect(list).toHaveAttribute('aria-orientation', orientation)
let activeTab = Array.from(list.querySelectorAll('[id^="headlessui-tabs-tab-"]'))[active] let activeTab = Array.from(list.querySelectorAll('[id^="headlessui-tabs-tab-"]'))[active]
let activePanel = panels.find(panel => panel.id === activeTab.getAttribute('aria-controls')) let activePanel = panels.find((panel) => panel.id === activeTab.getAttribute('aria-controls'))
for (let tab of tabs) { for (let tab of tabs) {
expect(tab).toHaveAttribute('id') expect(tab).toHaveAttribute('id')
@@ -18,7 +18,7 @@ function redentSnapshot(input: string) {
return input return input
.split('\n') .split('\n')
.map(line => .map((line) =>
line.trim() === '---' ? line : line.replace(replacer, (_, sign, rest) => `${sign} ${rest}`) line.trim() === '---' ? line : line.replace(replacer, (_, sign, rest) => `${sign} ${rest}`)
) )
.join('\n') .join('\n')
@@ -70,13 +70,13 @@ export async function executeTimeline(
.reduce((total, current) => total + current, 0) .reduce((total, current) => total + current, 0)
// Changes happen in the next frame // Changes happen in the next frame
await new Promise(resolve => d.nextFrame(resolve)) await new Promise((resolve) => d.nextFrame(resolve))
// We wait for the amount of the duration // We wait for the amount of the duration
await new Promise(resolve => d.setTimeout(resolve, totalDuration)) await new Promise((resolve) => d.setTimeout(resolve, totalDuration))
// We wait an additional next frame so that we know that we are done // We wait an additional next frame so that we know that we are done
await new Promise(resolve => d.nextFrame(resolve)) await new Promise((resolve) => d.nextFrame(resolve))
}, Promise.resolve()) }, Promise.resolve())
if (snapshots.length <= 0) { if (snapshots.length <= 0) {
@@ -128,7 +128,7 @@ export async function executeTimeline(
.replace(/Snapshot Diff:\n/g, '') .replace(/Snapshot Diff:\n/g, '')
) )
.split('\n') .split('\n')
.map(line => ` ${line}`) .map((line) => ` ${line}`)
.join('\n')}` .join('\n')}`
}) })
.filter(Boolean) .filter(Boolean)
@@ -1,12 +1,12 @@
import { render } from './vue-testing-library' import { render } from './vue-testing-library'
import { type, shift, Keys } from './interactions' import { type, shift, Keys } from './interactions'
import { defineComponent, h } from 'vue' import { ComponentOptionsWithoutProps, defineComponent, h } from 'vue'
type Events = 'onKeydown' | 'onKeyup' | 'onKeypress' | 'onClick' | 'onBlur' | 'onFocus' type Events = 'onKeydown' | 'onKeyup' | 'onKeypress' | 'onClick' | 'onBlur' | 'onFocus'
let events: Events[] = ['onKeydown', 'onKeyup', 'onKeypress', 'onClick', 'onBlur', 'onFocus'] let events: Events[] = ['onKeydown', 'onKeyup', 'onKeypress', 'onClick', 'onBlur', 'onFocus']
function renderTemplate(input: string | Partial<Parameters<typeof defineComponent>[0]>) { function renderTemplate(input: string | ComponentOptionsWithoutProps) {
let defaultComponents = {} let defaultComponents = {}
if (typeof input === 'string') { if (typeof input === 'string') {
@@ -164,7 +164,7 @@ describe('Keyboard', () => {
let state = { readyToCapture: false } let state = { readyToCapture: false }
function createProps(id: string) { function createProps(id: string) {
return events.reduce( return events.reduce<Record<string, string | ((event: any) => void)>>(
(props, name) => { (props, name) => {
props[name] = (event: any) => { props[name] = (event: any) => {
if (!state.readyToCapture) return if (!state.readyToCapture) return
@@ -202,7 +202,7 @@ describe('Keyboard', () => {
await type([key(input)]) await type([key(input)])
let expected = result.map(e => event(e)) let expected = result.map((e) => event(e))
expect(fired.length).toEqual(result.length) expect(fired.length).toEqual(result.length)
@@ -36,7 +36,7 @@ export function shift(event: Partial<KeyboardEvent>) {
} }
export function word(input: string): Partial<KeyboardEvent>[] { export function word(input: string): Partial<KeyboardEvent>[] {
let result = input.split('').map(key => ({ key })) let result = input.split('').map((key) => ({ key }))
d.enqueue(() => { d.enqueue(() => {
let element = document.activeElement let element = document.activeElement
@@ -152,7 +152,7 @@ export async function type(events: Partial<KeyboardEvent>[], element = document.
let actions = order[event.key!] ?? order[Default as any] let actions = order[event.key!] ?? order[Default as any]
for (let action of actions) { for (let action of actions) {
let checks = action.name.split('And') let checks = action.name.split('And')
if (checks.some(check => skip.has(check))) continue if (checks.some((check) => skip.has(check))) continue
let result = action(element, { let result = action(element, {
type: action.name, type: action.name,
@@ -344,8 +344,8 @@ let focusableSelector = [
? // TODO: Remove this once JSDOM fixes the issue where an element that is ? // TODO: Remove this once JSDOM fixes the issue where an element that is
// "hidden" can be the document.activeElement, because this is not possible // "hidden" can be the document.activeElement, because this is not possible
// in real browsers. // in real browsers.
selector => `${selector}:not([tabindex='-1']):not([style*='display: none'])` (selector) => `${selector}:not([tabindex='-1']):not([style*='display: none'])`
: selector => `${selector}:not([tabindex='-1'])` : (selector) => `${selector}:not([tabindex='-1'])`
) )
.join(',') .join(',')
@@ -5,10 +5,10 @@ type FunctionPropertyNames<T> = {
export function suppressConsoleLogs<T extends unknown[]>( export function suppressConsoleLogs<T extends unknown[]>(
cb: (...args: T) => void, cb: (...args: T) => void,
type: FunctionPropertyNames<typeof global.console> = 'warn' type: FunctionPropertyNames<typeof globalThis.console> = 'warn'
) { ) {
return (...args: T) => { return (...args: T) => {
let spy = jest.spyOn(global.console, type).mockImplementation(jest.fn()) let spy = jest.spyOn(globalThis.console, type).mockImplementation(jest.fn())
return new Promise<void>((resolve, reject) => { return new Promise<void>((resolve, reject) => {
Promise.resolve(cb(...args)).then(resolve, reject) Promise.resolve(cb(...args)).then(resolve, reject)
@@ -40,7 +40,7 @@ export function calculateActiveIndex<TItem>(
let nextActiveIndex = (() => { let nextActiveIndex = (() => {
switch (action.focus) { switch (action.focus) {
case Focus.First: case Focus.First:
return items.findIndex(item => !resolvers.resolveDisabled(item)) return items.findIndex((item) => !resolvers.resolveDisabled(item))
case Focus.Previous: { case Focus.Previous: {
let idx = items let idx = items
@@ -64,13 +64,13 @@ export function calculateActiveIndex<TItem>(
let idx = items let idx = items
.slice() .slice()
.reverse() .reverse()
.findIndex(item => !resolvers.resolveDisabled(item)) .findIndex((item) => !resolvers.resolveDisabled(item))
if (idx === -1) return idx if (idx === -1) return idx
return items.length - 1 - idx return items.length - 1 - idx
} }
case Focus.Specific: case Focus.Specific:
return items.findIndex(item => resolvers.resolveId(item) === action.id) return items.findIndex((item) => resolvers.resolveId(item) === action.id)
case Focus.Nothing: case Focus.Nothing:
return null return null
@@ -18,8 +18,8 @@ let focusableSelector = [
? // TODO: Remove this once JSDOM fixes the issue where an element that is ? // TODO: Remove this once JSDOM fixes the issue where an element that is
// "hidden" can be the document.activeElement, because this is not possible // "hidden" can be the document.activeElement, because this is not possible
// in real browsers. // in real browsers.
selector => `${selector}:not([tabindex='-1']):not([style*='display: none'])` (selector) => `${selector}:not([tabindex='-1']):not([style*='display: none'])`
: selector => `${selector}:not([tabindex='-1'])` : (selector) => `${selector}:not([tabindex='-1'])`
) )
.join(',') .join(',')
+1 -1
View File
@@ -12,7 +12,7 @@ export function match<TValue extends string | number = string, TReturnValue = un
`Tried to handle "${value}" but there is no handler defined. Only defined handlers are: ${Object.keys( `Tried to handle "${value}" but there is no handler defined. Only defined handlers are: ${Object.keys(
lookup lookup
) )
.map(key => `"${key}"`) .map((key) => `"${key}"`)
.join(', ')}.` .join(', ')}.`
) )
if (Error.captureStackTrace) Error.captureStackTrace(error, match) if (Error.captureStackTrace) Error.captureStackTrace(error, match)
@@ -1,4 +1,4 @@
import { defineComponent } from 'vue' import { defineComponent, ComponentOptionsWithoutProps } from 'vue'
import { render as testRender } from '../test-utils/vue-testing-library' import { render as testRender } from '../test-utils/vue-testing-library'
import { render } from './render' import { render } from './render'
@@ -13,7 +13,7 @@ let Dummy = defineComponent({
}, },
}) })
function renderTemplate(input: string | Partial<Parameters<typeof defineComponent>[0]>) { function renderTemplate(input: string | ComponentOptionsWithoutProps) {
let defaultComponents = { Dummy } let defaultComponents = { Dummy }
if (typeof input === 'string') { if (typeof input === 'string') {
@@ -34,9 +34,7 @@ describe('Validation', () => {
expect.hasAssertions() expect.hasAssertions()
renderTemplate({ renderTemplate({
template: html` template: html` <Dummy as="template" class="abc">Contents</Dummy> `,
<Dummy as="template" class="abc">Contents</Dummy>
`,
errorCaptured(err) { errorCaptured(err) {
expect(err as Error).toEqual( expect(err as Error).toEqual(
new Error( new Error(
+2 -2
View File
@@ -98,7 +98,7 @@ function _render({
`However we need to passthrough the following props:`, `However we need to passthrough the following props:`,
Object.keys(passThroughProps) Object.keys(passThroughProps)
.concat(Object.keys(attrs)) .concat(Object.keys(attrs))
.map(line => ` - ${line}`) .map((line) => ` - ${line}`)
.join('\n'), .join('\n'),
'', '',
'You can apply a few solutions:', 'You can apply a few solutions:',
@@ -106,7 +106,7 @@ function _render({
'Add an `as="..."` prop, to ensure that we render an actual element instead of a "template".', 'Add an `as="..."` prop, to ensure that we render an actual element instead of a "template".',
'Render a single element as the child so that we can forward the props onto that element.', 'Render a single element as the child so that we can forward the props onto that element.',
] ]
.map(line => ` - ${line}`) .map((line) => ` - ${line}`)
.join('\n'), .join('\n'),
].join('\n') ].join('\n')
) )
-1
View File
@@ -24,7 +24,6 @@
"allowJs": true, "allowJs": true,
"skipLibCheck": true, "skipLibCheck": true,
"forceConsistentCasingInFileNames": true, "forceConsistentCasingInFileNames": true,
"noEmit": true,
"resolveJsonModule": true, "resolveJsonModule": true,
"isolatedModules": true "isolatedModules": true
}, },

Some files were not shown because too many files have changed in this diff Show More