cleanup and consistency (#213)

- Made the use of `const` and `let` consistent
- import required functions and types from 'react' instead of using the
  `React.` namespace.
- Added `Expand` type, which can expand complex types to their "final"
  result.
- Ensured that we use `as const` for DEFAULT_XXX_TAG where we used a
  string. So that we have the type of `div` instead of `string` for
  example.
- Used `interface` over `type` where possible. I'm personally more of a
  `type` fan. But the TypeScript recommends `interfaces` where possible
  because they are faster, yield better error messages and so on.
This commit is contained in:
Robin Malfait
2021-01-30 14:46:54 +01:00
committed by GitHub
parent da179ca72b
commit ef00732685
78 changed files with 1115 additions and 1049 deletions
+6 -6
View File
@@ -1,17 +1,17 @@
import '@testing-library/jest-dom/extend-expect'
// Assuming requestAnimationFrame is roughly 60 frames per second
const frame = 1000 / 60
const amountOfFrames = 2
let frame = 1000 / 60
let amountOfFrames = 2
const formatter = new Intl.NumberFormat('en')
let formatter = new Intl.NumberFormat('en')
expect.extend({
toBeWithinRenderFrame(actual, expected) {
const min = expected - frame * amountOfFrames
const max = expected + frame * amountOfFrames
let min = expected - frame * amountOfFrames
let max = expected + frame * amountOfFrames
const pass = actual >= min && actual <= max
let pass = actual >= min && actual <= max
return {
message: pass
+9 -9
View File
@@ -1,4 +1,4 @@
import React from 'react'
import React, { useState, useEffect } from 'react'
import Link from 'next/link'
import Head from 'next/head'
@@ -7,7 +7,7 @@ import { useDisposables } from '../src/hooks/use-disposables'
import { PropsOf } from '../src/types'
function NextLink(props: PropsOf<'a'>) {
const { href, children, ...rest } = props
let { href, children, ...rest } = props
return (
<Link href={href}>
<a {...rest}>{children}</a>
@@ -58,23 +58,23 @@ function tap<T>(value: T, cb: (value: T) => void) {
}
function useKeyDisplay() {
const [mounted, setMounted] = React.useState(false)
let [mounted, setMounted] = useState(false)
React.useEffect(() => {
useEffect(() => {
setMounted(true)
}, [])
if (!mounted) return {}
const isMac = navigator.userAgent.indexOf('Mac OS X') !== -1
let isMac = navigator.userAgent.indexOf('Mac OS X') !== -1
return isMac ? KeyDisplayMac : KeyDisplayWindows
}
function KeyCaster() {
const [keys, setKeys] = React.useState<string[]>([])
const d = useDisposables()
const KeyDisplay = useKeyDisplay()
let [keys, setKeys] = useState<string[]>([])
let d = useDisposables()
let KeyDisplay = useKeyDisplay()
React.useEffect(() => {
useEffect(() => {
function handler(event: KeyboardEvent) {
setKeys(current => [
event.shiftKey && event.key !== 'Shift'
+4 -4
View File
@@ -1,5 +1,5 @@
import * as React from 'react'
import Error from 'next/error'
import React from 'react'
import ErrorPage from 'next/error'
import Head from 'next/head'
import Link from 'next/link'
@@ -7,7 +7,7 @@ import { ExamplesType, resolveAllExamples } from '../playground-utils/resolve-al
import { PropsOf } from '../src/types'
function NextLink(props: PropsOf<'a'>) {
const { href, children, ...rest } = props
let { href, children, ...rest } = props
return (
<Link href={href}>
<a {...rest}>{children}</a>
@@ -25,7 +25,7 @@ export async function getStaticProps() {
export default function Page(props: { examples: false | ExamplesType[] }) {
if (props.examples === false) {
return <Error statusCode={404} />
return <ErrorPage statusCode={404} />
}
return (
@@ -1,9 +1,9 @@
import * as React from 'react'
import React, { useState, useEffect } from 'react'
import { Listbox } from '@headlessui/react'
import { classNames } from '../../src/utils/class-names'
const people = [
let people = [
'Wade Cooper',
'Arlene Mccoy',
'Devon Webb',
@@ -17,10 +17,10 @@ const people = [
]
export default function Home() {
const [active, setActivePerson] = React.useState(people[2])
let [active, setActivePerson] = useState(people[2])
// Choose a random person on mount
React.useEffect(() => {
useEffect(() => {
setActivePerson(people[Math.floor(Math.random() * people.length)])
}, [])
@@ -1,11 +1,11 @@
import * as React from 'react'
import React, { useState, useEffect } from 'react'
import { Listbox } from '@headlessui/react'
function classNames(...classes) {
return classes.filter(Boolean).join(' ')
}
const people = [
let people = [
'Wade Cooper',
'Arlene Mccoy',
'Devon Webb',
@@ -41,10 +41,10 @@ export default function Home() {
}
function PeopleList() {
const [active, setActivePerson] = React.useState(people[2])
let [active, setActivePerson] = useState(people[2])
// Choose a random person on mount
React.useEffect(() => {
useEffect(() => {
setActivePerson(people[Math.floor(Math.random() * people.length)])
}, [])
@@ -1,4 +1,4 @@
import * as React from 'react'
import React from 'react'
import Link from 'next/link'
import { AnimatePresence, motion } from 'framer-motion'
import { Menu } from '../../src/components/menu/menu'
@@ -69,7 +69,7 @@ export default function Home() {
}
function NextLink(props: PropsOf<'a'>) {
const { href, children, ...rest } = props
let { href, children, ...rest } = props
return (
<Link href={href}>
<a {...rest}>{children}</a>
@@ -1,5 +1,5 @@
import * as React from 'react'
import * as ReactDOM from 'react-dom'
import React, { ReactNode, useState, useEffect } from 'react'
import { createPortal } from 'react-dom'
import { Menu } from '@headlessui/react'
import { usePopper } from '../../playground-utils/hooks/use-popper'
@@ -9,7 +9,7 @@ function classNames(...classes) {
}
export default function Home() {
const [trigger, container] = usePopper({
let [trigger, container] = usePopper({
placement: 'bottom-end',
strategy: 'fixed',
modifiers: [{ name: 'offset', options: { offset: [0, 10] } }],
@@ -87,12 +87,12 @@ export default function Home() {
)
}
function Portal(props: { children: React.ReactNode }) {
const { children } = props
const [mounted, setMounted] = React.useState(false)
function Portal(props: { children: ReactNode }) {
let { children } = props
let [mounted, setMounted] = useState(false)
React.useEffect(() => setMounted(true), [])
useEffect(() => setMounted(true), [])
if (!mounted) return null
return ReactDOM.createPortal(children, document.body)
return createPortal(children, document.body)
}
@@ -1,4 +1,4 @@
import * as React from 'react'
import React from 'react'
import { Menu, Transition } from '@headlessui/react'
import { usePopper } from '../../playground-utils/hooks/use-popper'
@@ -8,7 +8,7 @@ function classNames(...classes) {
}
export default function Home() {
const [trigger, container] = usePopper({
let [trigger, container] = usePopper({
placement: 'bottom-end',
strategy: 'fixed',
modifiers: [{ name: 'offset', options: { offset: [0, 10] } }],
@@ -1,4 +1,4 @@
import * as React from 'react'
import React from 'react'
import { Menu, Transition } from '@headlessui/react'
function classNames(...classes) {
@@ -1,4 +1,4 @@
import * as React from 'react'
import React from 'react'
import { Menu } from '@headlessui/react'
import { PropsOf } from '../../src/types'
@@ -55,7 +55,7 @@ export default function Home() {
)
}
function CustomMenuItem(props: PropsOf<Menu.Item<'a'>>) {
function CustomMenuItem(props: PropsOf<typeof Menu.Item>) {
return (
<Menu.Item {...props}>
{({ active, disabled }) => (
@@ -1,4 +1,4 @@
import * as React from 'react'
import React from 'react'
import { Menu } from '@headlessui/react'
function classNames(...classes) {
@@ -1,10 +1,10 @@
import * as React from 'react'
import React, { useState } from 'react'
import { Switch } from '@headlessui/react'
import { classNames } from '../../src/utils/class-names'
export default function Home() {
const [state, setState] = React.useState(false)
let [state, setState] = useState(false)
return (
<div className="flex items-start justify-center w-screen h-full p-12 bg-gray-50">
@@ -17,7 +17,7 @@ export default function Home() {
}
function Dropdown() {
const [isOpen, setIsOpen] = useState(false)
let [isOpen, setIsOpen] = useState(false)
return (
<div className="relative inline-block text-left">
@@ -2,14 +2,14 @@ import React, { useRef, useState } from 'react'
import { Transition } from '@headlessui/react'
export default function Home() {
const [isOpen, setIsOpen] = useState(false)
let [isOpen, setIsOpen] = useState(false)
function toggle() {
setIsOpen(v => !v)
}
const [email, setEmail] = useState('')
const [events, setEvents] = useState([])
const inputRef = useRef(null)
let [email, setEmail] = useState('')
let [events, setEvents] = useState([])
let inputRef = useRef(null)
function addEvent(name) {
setEvents(existing => [...existing, `${new Date().toJSON()} - ${name}`])
@@ -1,8 +1,8 @@
import React, { useState } from 'react'
import React, { useState, ReactNode } from 'react'
import { Transition } from '@headlessui/react'
export default function Home() {
const [isOpen, setIsOpen] = useState(true)
let [isOpen, setIsOpen] = useState(true)
return (
<>
@@ -40,7 +40,7 @@ export default function Home() {
)
}
function Box({ children }: { children?: React.ReactNode }) {
function Box({ children }: { children?: ReactNode }) {
return (
<Transition.Child
unmount={false}
@@ -1,8 +1,8 @@
import React, { useState } from 'react'
import React, { useState, ReactNode } from 'react'
import { Transition } from '@headlessui/react'
export default function Home() {
const [isOpen, setIsOpen] = useState(true)
let [isOpen, setIsOpen] = useState(true)
return (
<>
@@ -40,7 +40,7 @@ export default function Home() {
)
}
function Box({ children }: { children?: React.ReactNode }) {
function Box({ children }: { children?: ReactNode }) {
return (
<Transition.Child
unmount={true}
@@ -2,7 +2,7 @@ import React, { useState } from 'react'
import { Transition } from '@headlessui/react'
export default function Home() {
const [isOpen, setIsOpen] = useState(true)
let [isOpen, setIsOpen] = useState(true)
return (
<>
@@ -21,7 +21,7 @@ export default function Shell() {
}
function usePrevious<T>(value: T) {
const ref = useRef(value)
let ref = useRef(value)
useEffect(() => {
ref.current = value
}, [value])
@@ -33,8 +33,8 @@ enum Direction {
Backwards = ' <- ',
}
const pages = ['Dashboard', 'Team', 'Projects', 'Calendar', 'Reports']
const colors = [
let pages = ['Dashboard', 'Team', 'Projects', 'Calendar', 'Reports']
let colors = [
'bg-gradient-to-r from-teal-400 to-blue-400',
'bg-gradient-to-r from-blue-400 to-orange-400',
'bg-gradient-to-r from-orange-400 to-purple-400',
@@ -43,12 +43,12 @@ const colors = [
]
function FullPageTransition() {
const [activePage, setActivePage] = useState(0)
const previousPage = usePrevious(activePage)
let [activePage, setActivePage] = useState(0)
let previousPage = usePrevious(activePage)
const direction = activePage > previousPage ? Direction.Forwards : Direction.Backwards
let direction = activePage > previousPage ? Direction.Forwards : Direction.Backwards
const transitions = match(direction, {
let transitions = match(direction, {
[Direction.Forwards]: {
enter: 'transition transform ease-in-out duration-500',
enterFrom: 'translate-x-full',
@@ -3,7 +3,7 @@ import Head from 'next/head'
import { Transition } from '@headlessui/react'
export default function App() {
const [mobileOpen, setMobileOpen] = useState(false)
let [mobileOpen, setMobileOpen] = useState(false)
useEffect(() => {
function handleEscape(event) {
@@ -1,4 +1,4 @@
import React from 'react'
import { RefCallback, useRef, useCallback, useMemo } from 'react'
import { createPopper, Options } from '@popperjs/core'
/**
@@ -6,13 +6,13 @@ import { createPopper, Options } from '@popperjs/core'
*/
export function usePopper(
options?: Partial<Options>
): [React.RefCallback<Element | null>, React.RefCallback<HTMLElement | null>] {
const reference = React.useRef<Element>(null)
const popper = React.useRef<HTMLElement>(null)
): [RefCallback<Element | null>, RefCallback<HTMLElement | null>] {
let reference = useRef<Element>(null)
let popper = useRef<HTMLElement>(null)
const cleanupCallback = React.useRef(() => {})
let cleanupCallback = useRef(() => {})
const instantiatePopper = React.useCallback(() => {
let instantiatePopper = useCallback(() => {
if (!reference.current) return
if (!popper.current) return
@@ -21,7 +21,7 @@ export function usePopper(
cleanupCallback.current = createPopper(reference.current, popper.current, options).destroy
}, [reference, popper, cleanupCallback, options])
return React.useMemo(
return useMemo(
() => [
referenceDomNode => {
reference.current = referenceDomNode
@@ -7,14 +7,14 @@ export type ExamplesType = {
}
export async function resolveAllExamples(...paths: string[]) {
const base = path.resolve(process.cwd(), ...paths)
let base = path.resolve(process.cwd(), ...paths)
if (!fs.existsSync(base)) {
return false
}
const files = await fs.promises.readdir(base, { withFileTypes: true })
const items: ExamplesType[] = []
let files = await fs.promises.readdir(base, { withFileTypes: true })
let items: ExamplesType[] = []
for (let file of files) {
// Skip reserved filenames from Next. E.g.: _app.tsx, _error.tsx
@@ -22,7 +22,7 @@ export async function resolveAllExamples(...paths: string[]) {
continue
}
const bucket: ExamplesType = {
let bucket: ExamplesType = {
name: file.name.replace(/-/g, ' ').replace(/\.tsx?/g, ''),
path: [...paths, file.name]
.join('/')
@@ -32,7 +32,7 @@ export async function resolveAllExamples(...paths: string[]) {
}
if (file.isDirectory()) {
const children = await resolveAllExamples(...paths, file.name)
let children = await resolveAllExamples(...paths, file.name)
if (children) {
bucket.children = children
@@ -1,4 +1,4 @@
import React from 'react'
import React, { createElement, useState } from 'react'
import { render } from '@testing-library/react'
import { Listbox } from './listbox'
@@ -54,7 +54,7 @@ describe('safeguards', () => {
])(
'should error when we are using a <%s /> without a parent <Listbox />',
suppressConsoleLogs((name, Component) => {
expect(() => render(React.createElement(Component))).toThrowError(
expect(() => render(createElement(Component))).toThrowError(
`<${name} /> is missing a parent <Listbox /> component.`
)
})
@@ -426,7 +426,7 @@ describe('Rendering composition', () => {
// Open Listbox
await click(getListboxButton())
const options = getListboxOptions()
let options = getListboxOptions()
// Verify correct classNames
expect('' + options[0].classList).toEqual(
@@ -545,7 +545,7 @@ describe('Keyboard interactions', () => {
assertListboxButtonLinkedWithListbox()
// Verify we have listbox options
const options = getListboxOptions()
let options = getListboxOptions()
expect(options).toHaveLength(3)
options.forEach(option => assertListboxOption(option, { selected: false }))
@@ -626,7 +626,7 @@ describe('Keyboard interactions', () => {
assertListboxButtonLinkedWithListbox()
// Verify we have listbox options
const options = getListboxOptions()
let options = getListboxOptions()
expect(options).toHaveLength(3)
options.forEach((option, i) => assertListboxOption(option, { selected: i === 1 }))
@@ -670,7 +670,7 @@ describe('Keyboard interactions', () => {
assertActiveElement(getListbox())
assertListboxButtonLinkedWithListbox()
const options = getListboxOptions()
let options = getListboxOptions()
// Hover over Option A
await mouseMove(options[0])
@@ -699,12 +699,12 @@ describe('Keyboard interactions', () => {
it(
'should be possible to open the listbox with Enter, and focus the selected option (with a list of objects)',
suppressConsoleLogs(async () => {
const myOptions = [
let myOptions = [
{ id: 'a', name: 'Option A' },
{ id: 'b', name: 'Option B' },
{ id: 'c', name: 'Option C' },
]
const selectedOption = myOptions[1]
let selectedOption = myOptions[1]
render(
<Listbox value={selectedOption} onChange={console.log}>
<Listbox.Button>Trigger</Listbox.Button>
@@ -740,7 +740,7 @@ describe('Keyboard interactions', () => {
assertListboxButtonLinkedWithListbox()
// Verify we have listbox options
const options = getListboxOptions()
let options = getListboxOptions()
expect(options).toHaveLength(3)
options.forEach((option, i) => assertListboxOption(option, { selected: i === 1 }))
@@ -801,7 +801,7 @@ describe('Keyboard interactions', () => {
// Open listbox
await press(Keys.Enter)
const options = getListboxOptions()
let options = getListboxOptions()
// Verify that the first non-disabled listbox option is active
assertActiveListboxOption(options[1])
@@ -838,7 +838,7 @@ describe('Keyboard interactions', () => {
// Open listbox
await press(Keys.Enter)
const options = getListboxOptions()
let options = getListboxOptions()
// Verify that the first non-disabled listbox option is active
assertActiveListboxOption(options[2])
@@ -922,10 +922,10 @@ describe('Keyboard interactions', () => {
it(
'should be possible to close the listbox with Enter and choose the active listbox option',
suppressConsoleLogs(async () => {
const handleChange = jest.fn()
let handleChange = jest.fn()
function Example() {
const [value, setValue] = React.useState(undefined)
let [value, setValue] = useState(undefined)
return (
<Listbox
@@ -960,7 +960,7 @@ describe('Keyboard interactions', () => {
assertListboxButton({ state: ListboxState.Visible })
// Activate the first listbox option
const options = getListboxOptions()
let options = getListboxOptions()
await mouseMove(options[0])
// Choose option, and close listbox
@@ -1023,7 +1023,7 @@ describe('Keyboard interactions', () => {
assertListboxButtonLinkedWithListbox()
// Verify we have listbox options
const options = getListboxOptions()
let options = getListboxOptions()
expect(options).toHaveLength(3)
options.forEach(option => assertListboxOption(option))
assertActiveListboxOption(options[0])
@@ -1101,7 +1101,7 @@ describe('Keyboard interactions', () => {
assertListboxButtonLinkedWithListbox()
// Verify we have listbox options
const options = getListboxOptions()
let options = getListboxOptions()
expect(options).toHaveLength(3)
options.forEach((option, i) => assertListboxOption(option, { selected: i === 1 }))
@@ -1162,7 +1162,7 @@ describe('Keyboard interactions', () => {
// Open listbox
await press(Keys.Space)
const options = getListboxOptions()
let options = getListboxOptions()
// Verify that the first non-disabled listbox option is active
assertActiveListboxOption(options[1])
@@ -1199,7 +1199,7 @@ describe('Keyboard interactions', () => {
// Open listbox
await press(Keys.Space)
const options = getListboxOptions()
let options = getListboxOptions()
// Verify that the first non-disabled listbox option is active
assertActiveListboxOption(options[2])
@@ -1245,10 +1245,10 @@ describe('Keyboard interactions', () => {
it(
'should be possible to close the listbox with Space and choose the active listbox option',
suppressConsoleLogs(async () => {
const handleChange = jest.fn()
let handleChange = jest.fn()
function Example() {
const [value, setValue] = React.useState(undefined)
let [value, setValue] = useState(undefined)
return (
<Listbox
@@ -1283,7 +1283,7 @@ describe('Keyboard interactions', () => {
assertListboxButton({ state: ListboxState.Visible })
// Activate the first listbox option
const options = getListboxOptions()
let options = getListboxOptions()
await mouseMove(options[0])
// Choose option, and close listbox
@@ -1389,7 +1389,7 @@ describe('Keyboard interactions', () => {
assertListboxButtonLinkedWithListbox()
// Verify we have listbox options
const options = getListboxOptions()
let options = getListboxOptions()
expect(options).toHaveLength(3)
options.forEach(option => assertListboxOption(option))
assertActiveListboxOption(options[0])
@@ -1440,7 +1440,7 @@ describe('Keyboard interactions', () => {
assertListboxButtonLinkedWithListbox()
// Verify we have listbox options
const options = getListboxOptions()
let options = getListboxOptions()
expect(options).toHaveLength(3)
options.forEach(option => assertListboxOption(option))
assertActiveListboxOption(options[0])
@@ -1493,7 +1493,7 @@ describe('Keyboard interactions', () => {
assertListboxButtonLinkedWithListbox()
// Verify we have listbox options
const options = getListboxOptions()
let options = getListboxOptions()
expect(options).toHaveLength(3)
options.forEach(option => assertListboxOption(option))
@@ -1573,7 +1573,7 @@ describe('Keyboard interactions', () => {
assertListboxButtonLinkedWithListbox()
// Verify we have listbox options
const options = getListboxOptions()
let options = getListboxOptions()
expect(options).toHaveLength(3)
options.forEach((option, i) => assertListboxOption(option, { selected: i === 1 }))
@@ -1633,7 +1633,7 @@ describe('Keyboard interactions', () => {
await press(Keys.Enter)
// Verify we have listbox options
const options = getListboxOptions()
let options = getListboxOptions()
expect(options).toHaveLength(3)
options.forEach(option => assertListboxOption(option))
assertActiveListboxOption(options[0])
@@ -1681,7 +1681,7 @@ describe('Keyboard interactions', () => {
await press(Keys.Enter)
// Verify we have listbox options
const options = getListboxOptions()
let options = getListboxOptions()
expect(options).toHaveLength(3)
options.forEach(option => assertListboxOption(option))
assertActiveListboxOption(options[1])
@@ -1723,7 +1723,7 @@ describe('Keyboard interactions', () => {
await press(Keys.Enter)
// Verify we have listbox options
const options = getListboxOptions()
let options = getListboxOptions()
expect(options).toHaveLength(3)
options.forEach(option => assertListboxOption(option))
assertActiveListboxOption(options[2])
@@ -1768,7 +1768,7 @@ describe('Keyboard interactions', () => {
assertListboxButtonLinkedWithListbox()
// Verify we have listbox options
const options = getListboxOptions()
let options = getListboxOptions()
expect(options).toHaveLength(3)
options.forEach(option => assertListboxOption(option))
@@ -1848,7 +1848,7 @@ describe('Keyboard interactions', () => {
assertListboxButtonLinkedWithListbox()
// Verify we have listbox options
const options = getListboxOptions()
let options = getListboxOptions()
expect(options).toHaveLength(3)
options.forEach((option, i) => assertListboxOption(option, { selected: i === 1 }))
@@ -1912,7 +1912,7 @@ describe('Keyboard interactions', () => {
await press(Keys.ArrowUp)
// Verify we have listbox options
const options = getListboxOptions()
let options = getListboxOptions()
expect(options).toHaveLength(3)
options.forEach(option => assertListboxOption(option))
assertActiveListboxOption(options[0])
@@ -1950,7 +1950,7 @@ describe('Keyboard interactions', () => {
await press(Keys.Enter)
// Verify we have listbox options
const options = getListboxOptions()
let options = getListboxOptions()
expect(options).toHaveLength(3)
options.forEach(option => assertListboxOption(option))
assertActiveListboxOption(options[2])
@@ -2001,7 +2001,7 @@ describe('Keyboard interactions', () => {
assertListboxButtonLinkedWithListbox()
// Verify we have listbox options
const options = getListboxOptions()
let options = getListboxOptions()
expect(options).toHaveLength(3)
options.forEach(option => assertListboxOption(option))
assertActiveListboxOption(options[2])
@@ -2042,7 +2042,7 @@ describe('Keyboard interactions', () => {
// Open listbox
await press(Keys.Enter)
const options = getListboxOptions()
let options = getListboxOptions()
// We should be on the first option
assertActiveListboxOption(options[0])
@@ -2078,7 +2078,7 @@ describe('Keyboard interactions', () => {
// Open listbox
await press(Keys.Enter)
const options = getListboxOptions()
let options = getListboxOptions()
// We should be on the first option
assertActiveListboxOption(options[0])
@@ -2119,7 +2119,7 @@ describe('Keyboard interactions', () => {
// We should not be able to go to the end
await press(Keys.End)
const options = getListboxOptions()
let options = getListboxOptions()
assertActiveListboxOption(options[0])
})
)
@@ -2182,7 +2182,7 @@ describe('Keyboard interactions', () => {
// Open listbox
await press(Keys.Enter)
const options = getListboxOptions()
let options = getListboxOptions()
// We should be on the first option
assertActiveListboxOption(options[0])
@@ -2218,7 +2218,7 @@ describe('Keyboard interactions', () => {
// Open listbox
await press(Keys.Enter)
const options = getListboxOptions()
let options = getListboxOptions()
// We should be on the first option
assertActiveListboxOption(options[0])
@@ -2259,7 +2259,7 @@ describe('Keyboard interactions', () => {
// We should not be able to go to the end
await press(Keys.PageDown)
const options = getListboxOptions()
let options = getListboxOptions()
assertActiveListboxOption(options[0])
})
)
@@ -2322,7 +2322,7 @@ describe('Keyboard interactions', () => {
// Open listbox
await press(Keys.ArrowUp)
const options = getListboxOptions()
let options = getListboxOptions()
// We should be on the last option
assertActiveListboxOption(options[2])
@@ -2361,7 +2361,7 @@ describe('Keyboard interactions', () => {
// We should not be able to go to the end
await press(Keys.Home)
const options = getListboxOptions()
let options = getListboxOptions()
// We should be on the first non-disabled option
assertActiveListboxOption(options[2])
@@ -2398,7 +2398,7 @@ describe('Keyboard interactions', () => {
// We should not be able to go to the end
await press(Keys.Home)
const options = getListboxOptions()
let options = getListboxOptions()
assertActiveListboxOption(options[3])
})
)
@@ -2461,7 +2461,7 @@ describe('Keyboard interactions', () => {
// Open listbox
await press(Keys.ArrowUp)
const options = getListboxOptions()
let options = getListboxOptions()
// We should be on the last option
assertActiveListboxOption(options[2])
@@ -2500,7 +2500,7 @@ describe('Keyboard interactions', () => {
// We should not be able to go to the end
await press(Keys.PageUp)
const options = getListboxOptions()
let options = getListboxOptions()
// We should be on the first non-disabled option
assertActiveListboxOption(options[2])
@@ -2537,7 +2537,7 @@ describe('Keyboard interactions', () => {
// We should not be able to go to the end
await press(Keys.PageUp)
const options = getListboxOptions()
let options = getListboxOptions()
assertActiveListboxOption(options[3])
})
)
@@ -2597,7 +2597,7 @@ describe('Keyboard interactions', () => {
// Open listbox
await click(getListboxButton())
const options = getListboxOptions()
let options = getListboxOptions()
// We should be able to go to the second option
await type(word('bob'))
@@ -2633,7 +2633,7 @@ describe('Keyboard interactions', () => {
// Open listbox
await press(Keys.ArrowUp)
const options = getListboxOptions()
let options = getListboxOptions()
// We should be on the last option
assertActiveListboxOption(options[2])
@@ -2672,7 +2672,7 @@ describe('Keyboard interactions', () => {
// Open listbox
await press(Keys.ArrowUp)
const options = getListboxOptions()
let options = getListboxOptions()
// We should be on the last option
assertActiveListboxOption(options[2])
@@ -2713,7 +2713,7 @@ describe('Keyboard interactions', () => {
// Open listbox
await press(Keys.ArrowUp)
const options = getListboxOptions()
let options = getListboxOptions()
// We should be on the last option
assertActiveListboxOption(options[2])
@@ -2814,7 +2814,7 @@ describe('Mouse interactions', () => {
assertListboxButtonLinkedWithListbox()
// Verify we have listbox options
const options = getListboxOptions()
let options = getListboxOptions()
expect(options).toHaveLength(3)
options.forEach(option => assertListboxOption(option))
})
@@ -2913,7 +2913,7 @@ describe('Mouse interactions', () => {
assertListboxButtonLinkedWithListbox()
// Verify we have listbox options
const options = getListboxOptions()
let options = getListboxOptions()
expect(options).toHaveLength(3)
options.forEach((option, i) => assertListboxOption(option, { selected: i === 1 }))
@@ -3031,7 +3031,7 @@ describe('Mouse interactions', () => {
</div>
)
const [button1, button2] = getListboxButtons()
let [button1, button2] = getListboxButtons()
// Click the first menu button
await click(button1)
@@ -3097,7 +3097,7 @@ describe('Mouse interactions', () => {
// Open listbox
await click(getListboxButton())
const options = getListboxOptions()
let options = getListboxOptions()
// We should be able to go to the second option
await mouseMove(options[1])
assertActiveListboxOption(options[1])
@@ -3129,7 +3129,7 @@ describe('Mouse interactions', () => {
// Open listbox
await click(getListboxButton())
const options = getListboxOptions()
let options = getListboxOptions()
// We should be able to go to the second option
await mouseMove(options[1])
assertActiveListboxOption(options[1])
@@ -3153,7 +3153,7 @@ describe('Mouse interactions', () => {
// Open listbox
await click(getListboxButton())
const options = getListboxOptions()
let options = getListboxOptions()
// We should be able to go to the second option
await mouseMove(options[1])
@@ -3185,7 +3185,7 @@ describe('Mouse interactions', () => {
// Open listbox
await click(getListboxButton())
const options = getListboxOptions()
let options = getListboxOptions()
await mouseMove(options[1])
assertNoActiveListboxOption()
@@ -3211,7 +3211,7 @@ describe('Mouse interactions', () => {
// Open listbox
await click(getListboxButton())
const options = getListboxOptions()
let options = getListboxOptions()
// Try to hover over option 1, which is disabled
await mouseMove(options[1])
@@ -3238,7 +3238,7 @@ describe('Mouse interactions', () => {
// Open listbox
await click(getListboxButton())
const options = getListboxOptions()
let options = getListboxOptions()
// We should be able to go to the second option
await mouseMove(options[1])
@@ -3282,7 +3282,7 @@ describe('Mouse interactions', () => {
// Open listbox
await click(getListboxButton())
const options = getListboxOptions()
let options = getListboxOptions()
// Try to hover over option 1, which is disabled
await mouseMove(options[1])
@@ -3296,9 +3296,9 @@ describe('Mouse interactions', () => {
it(
'should be possible to click a listbox option, which closes the listbox',
suppressConsoleLogs(async () => {
const handleChange = jest.fn()
let handleChange = jest.fn()
function Example() {
const [value, setValue] = React.useState(undefined)
let [value, setValue] = useState(undefined)
return (
<Listbox
@@ -3325,7 +3325,7 @@ describe('Mouse interactions', () => {
assertListbox({ state: ListboxState.Visible })
assertActiveElement(getListbox())
const options = getListboxOptions()
let options = getListboxOptions()
// We should be able to click the first option
await click(options[1])
@@ -3347,9 +3347,9 @@ describe('Mouse interactions', () => {
it(
'should be possible to click a disabled listbox option, which is a no-op',
suppressConsoleLogs(async () => {
const handleChange = jest.fn()
let handleChange = jest.fn()
function Example() {
const [value, setValue] = React.useState(undefined)
let [value, setValue] = useState(undefined)
return (
<Listbox
@@ -3378,7 +3378,7 @@ describe('Mouse interactions', () => {
assertListbox({ state: ListboxState.Visible })
assertActiveElement(getListbox())
const options = getListboxOptions()
let options = getListboxOptions()
// We should be able to click the first option
await click(options[1])
@@ -3416,7 +3416,7 @@ describe('Mouse interactions', () => {
assertListbox({ state: ListboxState.Visible })
assertActiveElement(getListbox())
const options = getListboxOptions()
let options = getListboxOptions()
// Verify that nothing is active yet
assertNoActiveListboxOption()
@@ -3448,7 +3448,7 @@ describe('Mouse interactions', () => {
assertListbox({ state: ListboxState.Visible })
assertActiveElement(getListbox())
const options = getListboxOptions()
let options = getListboxOptions()
// We should not be able to focus the first option
await focus(options[1])
@@ -1,4 +1,22 @@
import * as React from 'react'
import React, {
createContext,
createRef,
useCallback,
useContext,
useEffect,
useMemo,
useReducer,
useRef,
Fragment,
// Types
Dispatch,
ElementType,
KeyboardEvent as ReactKeyboardEvent,
MouseEvent as ReactMouseEvent,
MutableRefObject,
Ref,
} from 'react'
import { useDisposables } from '../../hooks/use-disposables'
import { useId } from '../../hooks/use-id'
@@ -19,18 +37,18 @@ enum ListboxStates {
Closed,
}
type ListboxOptionDataRef = React.MutableRefObject<{
type ListboxOptionDataRef = MutableRefObject<{
textValue?: string
disabled: boolean
value: unknown
}>
type StateDefinition = {
interface StateDefinition {
listboxState: ListboxStates
propsRef: React.MutableRefObject<{ value: unknown; onChange(value: unknown): void }>
labelRef: React.MutableRefObject<HTMLLabelElement | null>
buttonRef: React.MutableRefObject<HTMLButtonElement | null>
optionsRef: React.MutableRefObject<HTMLUListElement | null>
propsRef: MutableRefObject<{ value: unknown; onChange(value: unknown): void }>
labelRef: MutableRefObject<HTMLLabelElement | null>
buttonRef: MutableRefObject<HTMLButtonElement | null>
optionsRef: MutableRefObject<HTMLUListElement | null>
options: { id: string; dataRef: ListboxOptionDataRef }[]
searchQuery: string
activeOptionIndex: number | null
@@ -58,7 +76,7 @@ type Actions =
| { type: ActionTypes.RegisterOption; id: string; dataRef: ListboxOptionDataRef }
| { type: ActionTypes.UnregisterOption; id: string }
const reducers: {
let reducers: {
[P in ActionTypes]: (
state: StateDefinition,
action: Extract<Actions, { type: P }>
@@ -71,7 +89,7 @@ const reducers: {
}),
[ActionTypes.OpenListbox]: state => ({ ...state, listboxState: ListboxStates.Open }),
[ActionTypes.GoToOption]: (state, action) => {
const activeOptionIndex = calculateActiveIndex(action, {
let activeOptionIndex = calculateActiveIndex(action, {
resolveItems: () => state.options,
resolveActiveIndex: () => state.activeOptionIndex,
resolveId: item => item.id,
@@ -82,8 +100,8 @@ const reducers: {
return { ...state, searchQuery: '', activeOptionIndex }
},
[ActionTypes.Search]: (state, action) => {
const searchQuery = state.searchQuery + action.value
const match = state.options.findIndex(
let searchQuery = state.searchQuery + action.value
let match = state.options.findIndex(
option =>
!option.dataRef.current.disabled &&
option.dataRef.current.textValue?.startsWith(searchQuery)
@@ -98,11 +116,11 @@ const reducers: {
options: [...state.options, { id: action.id, dataRef: action.dataRef }],
}),
[ActionTypes.UnregisterOption]: (state, action) => {
const nextOptions = state.options.slice()
const currentActiveOption =
let nextOptions = state.options.slice()
let currentActiveOption =
state.activeOptionIndex !== null ? nextOptions[state.activeOptionIndex] : null
const idx = nextOptions.findIndex(a => a.id === action.id)
let idx = nextOptions.findIndex(a => a.id === action.id)
if (idx !== -1) nextOptions.splice(idx, 1)
@@ -121,13 +139,13 @@ const reducers: {
},
}
const ListboxContext = React.createContext<[StateDefinition, React.Dispatch<Actions>] | null>(null)
let ListboxContext = createContext<[StateDefinition, Dispatch<Actions>] | null>(null)
ListboxContext.displayName = 'ListboxContext'
function useListboxContext(component: string) {
const context = React.useContext(ListboxContext)
let context = useContext(ListboxContext)
if (context === null) {
const err = new Error(`<${component} /> is missing a parent <${Listbox.name} /> component.`)
let err = new Error(`<${component} /> is missing a parent <${Listbox.name} /> component.`)
if (Error.captureStackTrace) Error.captureStackTrace(err, useListboxContext)
throw err
}
@@ -140,31 +158,30 @@ function stateReducer(state: StateDefinition, action: Actions) {
// ---
const DEFAULT_LISTBOX_TAG = React.Fragment
type ListboxRenderPropArg = { open: boolean }
let DEFAULT_LISTBOX_TAG = Fragment
interface ListboxRenderPropArg {
open: boolean
}
export function Listbox<
TTag extends React.ElementType = typeof DEFAULT_LISTBOX_TAG,
TType = string
>(
export function Listbox<TTag extends ElementType = typeof DEFAULT_LISTBOX_TAG, TType = string>(
props: Props<TTag, ListboxRenderPropArg, 'value' | 'onChange'> & {
value: TType
onChange(value: TType): void
}
) {
const { value, onChange, ...passThroughProps } = props
const d = useDisposables()
const reducerBag = React.useReducer(stateReducer, {
let { value, onChange, ...passThroughProps } = props
let d = useDisposables()
let reducerBag = useReducer(stateReducer, {
listboxState: ListboxStates.Closed,
propsRef: { current: { value, onChange } },
labelRef: React.createRef(),
buttonRef: React.createRef(),
optionsRef: React.createRef(),
labelRef: createRef(),
buttonRef: createRef(),
optionsRef: createRef(),
options: [],
searchQuery: '',
activeOptionIndex: null,
} as StateDefinition)
const [{ listboxState, propsRef, optionsRef, buttonRef }, dispatch] = reducerBag
let [{ listboxState, propsRef, optionsRef, buttonRef }, dispatch] = reducerBag
useIsoMorphicEffect(() => {
propsRef.current.value = value
@@ -173,10 +190,10 @@ export function Listbox<
propsRef.current.onChange = onChange
}, [onChange, propsRef])
React.useEffect(() => {
useEffect(() => {
function handler(event: MouseEvent) {
const target = event.target as HTMLElement
const active = document.activeElement
let target = event.target as HTMLElement
let active = document.activeElement
if (listboxState !== ListboxStates.Open) return
if (buttonRef.current?.contains(target)) return
@@ -190,7 +207,7 @@ export function Listbox<
return () => window.removeEventListener('mousedown', handler)
}, [listboxState, optionsRef, buttonRef, d, dispatch])
const propsBag = React.useMemo<ListboxRenderPropArg>(
let propsBag = useMemo<ListboxRenderPropArg>(
() => ({ open: listboxState === ListboxStates.Open }),
[listboxState]
)
@@ -204,8 +221,10 @@ export function Listbox<
// ---
const DEFAULT_BUTTON_TAG = 'button'
type ButtonRenderPropArg = { open: boolean }
let DEFAULT_BUTTON_TAG = 'button' as const
interface ButtonRenderPropArg {
open: boolean
}
type ButtonPropsWeControl =
| 'id'
| 'type'
@@ -216,20 +235,18 @@ type ButtonPropsWeControl =
| 'onKeyDown'
| 'onClick'
const Button = forwardRefWithAs(function Button<
TTag extends React.ElementType = typeof DEFAULT_BUTTON_TAG
>(
let Button = forwardRefWithAs(function Button<TTag extends ElementType = typeof DEFAULT_BUTTON_TAG>(
props: Props<TTag, ButtonRenderPropArg, ButtonPropsWeControl>,
ref: React.Ref<HTMLButtonElement>
ref: Ref<HTMLButtonElement>
) {
const [state, dispatch] = useListboxContext([Listbox.name, Button.name].join('.'))
const buttonRef = useSyncRefs(state.buttonRef, ref)
let [state, dispatch] = useListboxContext([Listbox.name, Button.name].join('.'))
let buttonRef = useSyncRefs(state.buttonRef, ref)
const id = `headlessui-listbox-button-${useId()}`
const d = useDisposables()
let id = `headlessui-listbox-button-${useId()}`
let d = useDisposables()
const handleKeyDown = React.useCallback(
(event: React.KeyboardEvent<HTMLButtonElement>) => {
let handleKeyDown = useCallback(
(event: ReactKeyboardEvent<HTMLButtonElement>) => {
switch (event.key) {
// Ref: https://www.w3.org/TR/wai-aria-practices-1.2/#keyboard-interaction-13
@@ -259,8 +276,8 @@ const Button = forwardRefWithAs(function Button<
[dispatch, state, d]
)
const handleClick = React.useCallback(
(event: React.MouseEvent) => {
let handleClick = useCallback(
(event: ReactMouseEvent) => {
if (isDisabledReactIssue7711(event.currentTarget)) return event.preventDefault()
if (props.disabled) return
if (state.listboxState === ListboxStates.Open) {
@@ -275,17 +292,17 @@ const Button = forwardRefWithAs(function Button<
[dispatch, d, state, props.disabled]
)
const labelledby = useComputed(() => {
let labelledby = useComputed(() => {
if (!state.labelRef.current) return undefined
return [state.labelRef.current.id, id].join(' ')
}, [state.labelRef.current, id])
const propsBag = React.useMemo<ButtonRenderPropArg>(
let propsBag = useMemo<ButtonRenderPropArg>(
() => ({ open: state.listboxState === ListboxStates.Open }),
[state]
)
const passthroughProps = props
const propsWeControl = {
let passthroughProps = props
let propsWeControl = {
ref: buttonRef,
id,
type: 'button',
@@ -302,33 +319,36 @@ const Button = forwardRefWithAs(function Button<
// ---
const DEFAULT_LABEL_TAG = 'label'
let DEFAULT_LABEL_TAG = 'label' as const
interface LabelRenderPropArg {
open: boolean
}
type LabelPropsWeControl = 'id' | 'ref' | 'onClick'
type LabelRenderPropArg = { open: boolean }
function Label<TTag extends React.ElementType = typeof DEFAULT_LABEL_TAG>(
function Label<TTag extends ElementType = typeof DEFAULT_LABEL_TAG>(
props: Props<TTag, LabelRenderPropArg, LabelPropsWeControl>
) {
const [state] = useListboxContext([Listbox.name, Label.name].join('.'))
const id = `headlessui-listbox-label-${useId()}`
let [state] = useListboxContext([Listbox.name, Label.name].join('.'))
let id = `headlessui-listbox-label-${useId()}`
const handleClick = React.useCallback(
() => state.buttonRef.current?.focus({ preventScroll: true }),
[state.buttonRef]
)
let handleClick = useCallback(() => state.buttonRef.current?.focus({ preventScroll: true }), [
state.buttonRef,
])
const propsBag = React.useMemo<OptionsRenderPropArg>(
let propsBag = useMemo<OptionsRenderPropArg>(
() => ({ open: state.listboxState === ListboxStates.Open }),
[state]
)
const propsWeControl = { ref: state.labelRef, id, onClick: handleClick }
let propsWeControl = { ref: state.labelRef, id, onClick: handleClick }
return render({ ...props, ...propsWeControl }, propsBag, DEFAULT_LABEL_TAG)
}
// ---
const DEFAULT_OPTIONS_TAG = 'ul'
type OptionsRenderPropArg = { open: boolean }
let DEFAULT_OPTIONS_TAG = 'ul' as const
interface OptionsRenderPropArg {
open: boolean
}
type OptionsPropsWeControl =
| 'aria-activedescendant'
| 'aria-labelledby'
@@ -337,24 +357,24 @@ type OptionsPropsWeControl =
| 'role'
| 'tabIndex'
const OptionsRenderFeatures = Features.RenderStrategy | Features.Static
let OptionsRenderFeatures = Features.RenderStrategy | Features.Static
const Options = forwardRefWithAs(function Options<
TTag extends React.ElementType = typeof DEFAULT_OPTIONS_TAG
let Options = forwardRefWithAs(function Options<
TTag extends ElementType = typeof DEFAULT_OPTIONS_TAG
>(
props: Props<TTag, OptionsRenderPropArg, OptionsPropsWeControl> &
PropsForFeatures<typeof OptionsRenderFeatures>,
ref: React.Ref<HTMLUListElement>
ref: Ref<HTMLUListElement>
) {
const [state, dispatch] = useListboxContext([Listbox.name, Options.name].join('.'))
const optionsRef = useSyncRefs(state.optionsRef, ref)
let [state, dispatch] = useListboxContext([Listbox.name, Options.name].join('.'))
let optionsRef = useSyncRefs(state.optionsRef, ref)
const id = `headlessui-listbox-options-${useId()}`
const d = useDisposables()
const searchDisposables = useDisposables()
let id = `headlessui-listbox-options-${useId()}`
let d = useDisposables()
let searchDisposables = useDisposables()
const handleKeyDown = React.useCallback(
(event: React.KeyboardEvent<HTMLUListElement>) => {
let handleKeyDown = useCallback(
(event: ReactKeyboardEvent<HTMLUListElement>) => {
searchDisposables.dispose()
switch (event.key) {
@@ -371,7 +391,7 @@ const Options = forwardRefWithAs(function Options<
event.preventDefault()
dispatch({ type: ActionTypes.CloseListbox })
if (state.activeOptionIndex !== null) {
const { dataRef } = state.options[state.activeOptionIndex]
let { dataRef } = state.options[state.activeOptionIndex]
state.propsRef.current.onChange(dataRef.current.value)
}
disposables().nextFrame(() => state.buttonRef.current?.focus({ preventScroll: true }))
@@ -414,16 +434,16 @@ const Options = forwardRefWithAs(function Options<
[d, dispatch, searchDisposables, state]
)
const labelledby = useComputed(() => state.labelRef.current?.id ?? state.buttonRef.current?.id, [
let labelledby = useComputed(() => state.labelRef.current?.id ?? state.buttonRef.current?.id, [
state.labelRef.current,
state.buttonRef.current,
])
const propsBag = React.useMemo<OptionsRenderPropArg>(
let propsBag = useMemo<OptionsRenderPropArg>(
() => ({ open: state.listboxState === ListboxStates.Open }),
[state]
)
const propsWeControl = {
let propsWeControl = {
'aria-activedescendant':
state.activeOptionIndex === null ? undefined : state.options[state.activeOptionIndex]?.id,
'aria-labelledby': labelledby,
@@ -433,7 +453,7 @@ const Options = forwardRefWithAs(function Options<
tabIndex: 0,
ref: optionsRef,
}
const passthroughProps = props
let passthroughProps = props
return render(
{ ...passthroughProps, ...propsWeControl },
@@ -446,8 +466,12 @@ const Options = forwardRefWithAs(function Options<
// ---
const DEFAULT_OPTION_TAG = 'li'
type OptionRenderPropArg = { active: boolean; selected: boolean; disabled: boolean }
let DEFAULT_OPTION_TAG = 'li' as const
interface OptionRenderPropArg {
active: boolean
selected: boolean
disabled: boolean
}
type ListboxOptionPropsWeControl =
| 'id'
| 'role'
@@ -461,7 +485,7 @@ type ListboxOptionPropsWeControl =
| 'onFocus'
function Option<
TTag extends React.ElementType = typeof DEFAULT_OPTION_TAG,
TTag extends ElementType = typeof DEFAULT_OPTION_TAG,
// TODO: One day we will be able to infer this type from the generic in Listbox itself.
// But today is not that day..
TType = Parameters<typeof Listbox>[0]['value']
@@ -474,14 +498,14 @@ function Option<
className?: ((bag: OptionRenderPropArg) => string) | string
}
) {
const { disabled = false, value, className, ...passthroughProps } = props
const [state, dispatch] = useListboxContext([Listbox.name, Option.name].join('.'))
const id = `headlessui-listbox-option-${useId()}`
const active =
let { disabled = false, value, className, ...passthroughProps } = props
let [state, dispatch] = useListboxContext([Listbox.name, Option.name].join('.'))
let id = `headlessui-listbox-option-${useId()}`
let active =
state.activeOptionIndex !== null ? state.options[state.activeOptionIndex].id === id : false
const selected = state.propsRef.current.value === value
let selected = state.propsRef.current.value === value
const bag = React.useRef<ListboxOptionDataRef['current']>({ disabled, value })
let bag = useRef<ListboxOptionDataRef['current']>({ disabled, value })
useIsoMorphicEffect(() => {
bag.current.disabled = disabled
@@ -493,10 +517,7 @@ function Option<
bag.current.textValue = document.getElementById(id)?.textContent?.toLowerCase()
}, [bag, id])
const select = React.useCallback(() => state.propsRef.current.onChange(value), [
state.propsRef,
value,
])
let select = useCallback(() => state.propsRef.current.onChange(value), [state.propsRef, value])
useIsoMorphicEffect(() => {
dispatch({ type: ActionTypes.RegisterOption, id, dataRef: bag })
@@ -513,12 +534,12 @@ function Option<
useIsoMorphicEffect(() => {
if (state.listboxState !== ListboxStates.Open) return
if (!active) return
const d = disposables()
let d = disposables()
d.nextFrame(() => document.getElementById(id)?.scrollIntoView?.({ block: 'nearest' }))
return d.dispose
}, [active, state.listboxState])
const handleClick = React.useCallback(
let handleClick = useCallback(
(event: { preventDefault: Function }) => {
if (disabled) return event.preventDefault()
select()
@@ -528,29 +549,25 @@ function Option<
[dispatch, state.buttonRef, disabled, select]
)
const handleFocus = React.useCallback(() => {
let handleFocus = useCallback(() => {
if (disabled) return dispatch({ type: ActionTypes.GoToOption, focus: Focus.Nothing })
dispatch({ type: ActionTypes.GoToOption, focus: Focus.Specific, id })
}, [disabled, id, dispatch])
const handleMove = React.useCallback(() => {
let handleMove = useCallback(() => {
if (disabled) return
if (active) return
dispatch({ type: ActionTypes.GoToOption, focus: Focus.Specific, id })
}, [disabled, active, id, dispatch])
const handleLeave = React.useCallback(() => {
let handleLeave = useCallback(() => {
if (disabled) return
if (!active) return
dispatch({ type: ActionTypes.GoToOption, focus: Focus.Nothing })
}, [disabled, active, dispatch])
const propsBag = React.useMemo(() => ({ active, selected, disabled }), [
active,
selected,
disabled,
])
const propsWeControl = {
let propsBag = useMemo(() => ({ active, selected, disabled }), [active, selected, disabled])
let propsWeControl = {
id,
role: 'option',
tabIndex: -1,
@@ -1,4 +1,4 @@
import React from 'react'
import React, { createElement } from 'react'
import { render } from '@testing-library/react'
import { Menu } from './menu'
@@ -48,7 +48,7 @@ describe('Safe guards', () => {
])(
'should error when we are using a <%s /> without a parent <Menu />',
suppressConsoleLogs((name, Component) => {
expect(() => render(React.createElement(Component))).toThrowError(
expect(() => render(createElement(Component))).toThrowError(
`<${name} /> is missing a parent <Menu /> component.`
)
})
@@ -321,7 +321,7 @@ describe('Rendering composition', () => {
// Open menu
await click(getMenuButton())
const items = getMenuItems()
let items = getMenuItems()
// Verify correct classNames
expect('' + items[0].classList).toEqual(JSON.stringify({ active: false, disabled: false }))
@@ -379,7 +379,7 @@ describe('Rendering composition', () => {
await click(getMenuButton())
// Verify items are buttons now
const items = getMenuItems()
let items = getMenuItems()
items.forEach(item => assertMenuItem(item, { tag: 'button' }))
})
)
@@ -422,7 +422,7 @@ describe('Keyboard interactions', () => {
assertMenuButtonLinkedWithMenu()
// Verify we have menu items
const items = getMenuItems()
let items = getMenuItems()
expect(items).toHaveLength(3)
items.forEach(item => assertMenuItem(item))
@@ -517,7 +517,7 @@ describe('Keyboard interactions', () => {
// Open menu
await press(Keys.Enter)
const items = getMenuItems()
let items = getMenuItems()
// Verify that the first non-disabled menu item is active
assertMenuLinkedWithMenuItem(items[1])
@@ -554,7 +554,7 @@ describe('Keyboard interactions', () => {
// Open menu
await press(Keys.Enter)
const items = getMenuItems()
let items = getMenuItems()
// Verify that the first non-disabled menu item is active
assertMenuLinkedWithMenuItem(items[2])
@@ -638,7 +638,7 @@ describe('Keyboard interactions', () => {
it(
'should be possible to close the menu with Enter and invoke the active menu item',
suppressConsoleLogs(async () => {
const clickHandler = jest.fn()
let clickHandler = jest.fn()
render(
<Menu>
<Menu.Button>Trigger</Menu.Button>
@@ -665,7 +665,7 @@ describe('Keyboard interactions', () => {
assertMenuButton({ state: MenuState.Visible })
// Activate the first menu item
const items = getMenuItems()
let items = getMenuItems()
await mouseMove(items[0])
// Close menu, and invoke the item
@@ -687,7 +687,7 @@ describe('Keyboard interactions', () => {
it(
'should be possible to use a button as a menu item and invoke it upon Enter',
suppressConsoleLogs(async () => {
const clickHandler = jest.fn()
let clickHandler = jest.fn()
render(
<Menu>
@@ -717,7 +717,7 @@ describe('Keyboard interactions', () => {
assertMenuButton({ state: MenuState.Visible })
// Activate the second menu item
const items = getMenuItems()
let items = getMenuItems()
await mouseMove(items[1])
// Close menu, and invoke the item
@@ -786,7 +786,7 @@ describe('Keyboard interactions', () => {
assertMenuButtonLinkedWithMenu()
// Verify we have menu items
const items = getMenuItems()
let items = getMenuItems()
expect(items).toHaveLength(3)
items.forEach(item => assertMenuItem(item))
assertMenuLinkedWithMenuItem(items[0])
@@ -879,7 +879,7 @@ describe('Keyboard interactions', () => {
// Open menu
await press(Keys.Space)
const items = getMenuItems()
let items = getMenuItems()
// Verify that the first non-disabled menu item is active
assertMenuLinkedWithMenuItem(items[1])
@@ -916,7 +916,7 @@ describe('Keyboard interactions', () => {
// Open menu
await press(Keys.Space)
const items = getMenuItems()
let items = getMenuItems()
// Verify that the first non-disabled menu item is active
assertMenuLinkedWithMenuItem(items[2])
@@ -1000,7 +1000,7 @@ describe('Keyboard interactions', () => {
it(
'should be possible to close the menu with Space and invoke the active menu item',
suppressConsoleLogs(async () => {
const clickHandler = jest.fn()
let clickHandler = jest.fn()
render(
<Menu>
<Menu.Button>Trigger</Menu.Button>
@@ -1027,7 +1027,7 @@ describe('Keyboard interactions', () => {
assertMenuButton({ state: MenuState.Visible })
// Activate the first menu item
const items = getMenuItems()
let items = getMenuItems()
await mouseMove(items[0])
// Close menu, and invoke the item
@@ -1124,7 +1124,7 @@ describe('Keyboard interactions', () => {
assertMenuButtonLinkedWithMenu()
// Verify we have menu items
const items = getMenuItems()
let items = getMenuItems()
expect(items).toHaveLength(3)
items.forEach(item => assertMenuItem(item))
assertMenuLinkedWithMenuItem(items[0])
@@ -1173,7 +1173,7 @@ describe('Keyboard interactions', () => {
assertMenuButtonLinkedWithMenu()
// Verify we have menu items
const items = getMenuItems()
let items = getMenuItems()
expect(items).toHaveLength(3)
items.forEach(item => assertMenuItem(item))
assertMenuLinkedWithMenuItem(items[0])
@@ -1224,7 +1224,7 @@ describe('Keyboard interactions', () => {
assertMenuButtonLinkedWithMenu()
// Verify we have menu items
const items = getMenuItems()
let items = getMenuItems()
expect(items).toHaveLength(3)
items.forEach(item => assertMenuItem(item))
@@ -1318,7 +1318,7 @@ describe('Keyboard interactions', () => {
await press(Keys.Enter)
// Verify we have menu items
const items = getMenuItems()
let items = getMenuItems()
expect(items).toHaveLength(3)
items.forEach(item => assertMenuItem(item))
assertMenuLinkedWithMenuItem(items[0])
@@ -1366,7 +1366,7 @@ describe('Keyboard interactions', () => {
await press(Keys.Enter)
// Verify we have menu items
const items = getMenuItems()
let items = getMenuItems()
expect(items).toHaveLength(3)
items.forEach(item => assertMenuItem(item))
assertMenuLinkedWithMenuItem(items[1])
@@ -1408,7 +1408,7 @@ describe('Keyboard interactions', () => {
await press(Keys.Enter)
// Verify we have menu items
const items = getMenuItems()
let items = getMenuItems()
expect(items).toHaveLength(3)
items.forEach(item => assertMenuItem(item))
assertMenuLinkedWithMenuItem(items[2])
@@ -1452,7 +1452,7 @@ describe('Keyboard interactions', () => {
assertMenuButtonLinkedWithMenu()
// Verify we have menu items
const items = getMenuItems()
let items = getMenuItems()
expect(items).toHaveLength(3)
items.forEach(item => assertMenuItem(item))
@@ -1550,7 +1550,7 @@ describe('Keyboard interactions', () => {
await press(Keys.ArrowUp)
// Verify we have menu items
const items = getMenuItems()
let items = getMenuItems()
expect(items).toHaveLength(3)
items.forEach(item => assertMenuItem(item))
assertMenuLinkedWithMenuItem(items[0])
@@ -1588,7 +1588,7 @@ describe('Keyboard interactions', () => {
await press(Keys.Enter)
// Verify we have menu items
const items = getMenuItems()
let items = getMenuItems()
expect(items).toHaveLength(3)
items.forEach(item => assertMenuItem(item))
assertMenuLinkedWithMenuItem(items[2])
@@ -1638,7 +1638,7 @@ describe('Keyboard interactions', () => {
assertMenuButtonLinkedWithMenu()
// Verify we have menu items
const items = getMenuItems()
let items = getMenuItems()
expect(items).toHaveLength(3)
items.forEach(item => assertMenuItem(item))
assertMenuLinkedWithMenuItem(items[2])
@@ -1679,7 +1679,7 @@ describe('Keyboard interactions', () => {
// Open menu
await press(Keys.Enter)
const items = getMenuItems()
let items = getMenuItems()
// We should be on the first item
assertMenuLinkedWithMenuItem(items[0])
@@ -1715,7 +1715,7 @@ describe('Keyboard interactions', () => {
// Open menu
await press(Keys.Enter)
const items = getMenuItems()
let items = getMenuItems()
// We should be on the first item
assertMenuLinkedWithMenuItem(items[0])
@@ -1756,7 +1756,7 @@ describe('Keyboard interactions', () => {
// We should not be able to go to the end
await press(Keys.End)
const items = getMenuItems()
let items = getMenuItems()
assertMenuLinkedWithMenuItem(items[0])
})
)
@@ -1819,7 +1819,7 @@ describe('Keyboard interactions', () => {
// Open menu
await press(Keys.Enter)
const items = getMenuItems()
let items = getMenuItems()
// We should be on the first item
assertMenuLinkedWithMenuItem(items[0])
@@ -1855,7 +1855,7 @@ describe('Keyboard interactions', () => {
// Open menu
await press(Keys.Enter)
const items = getMenuItems()
let items = getMenuItems()
// We should be on the first item
assertMenuLinkedWithMenuItem(items[0])
@@ -1896,7 +1896,7 @@ describe('Keyboard interactions', () => {
// We should not be able to go to the end
await press(Keys.PageDown)
const items = getMenuItems()
let items = getMenuItems()
assertMenuLinkedWithMenuItem(items[0])
})
)
@@ -1959,7 +1959,7 @@ describe('Keyboard interactions', () => {
// Open menu
await press(Keys.ArrowUp)
const items = getMenuItems()
let items = getMenuItems()
// We should be on the last item
assertMenuLinkedWithMenuItem(items[2])
@@ -1998,7 +1998,7 @@ describe('Keyboard interactions', () => {
// We should not be able to go to the end
await press(Keys.Home)
const items = getMenuItems()
let items = getMenuItems()
// We should be on the first non-disabled item
assertMenuLinkedWithMenuItem(items[2])
@@ -2035,7 +2035,7 @@ describe('Keyboard interactions', () => {
// We should not be able to go to the end
await press(Keys.Home)
const items = getMenuItems()
let items = getMenuItems()
assertMenuLinkedWithMenuItem(items[3])
})
)
@@ -2098,7 +2098,7 @@ describe('Keyboard interactions', () => {
// Open menu
await press(Keys.ArrowUp)
const items = getMenuItems()
let items = getMenuItems()
// We should be on the last item
assertMenuLinkedWithMenuItem(items[2])
@@ -2137,7 +2137,7 @@ describe('Keyboard interactions', () => {
// We should not be able to go to the end
await press(Keys.PageUp)
const items = getMenuItems()
let items = getMenuItems()
// We should be on the first non-disabled item
assertMenuLinkedWithMenuItem(items[2])
@@ -2174,7 +2174,7 @@ describe('Keyboard interactions', () => {
// We should not be able to go to the end
await press(Keys.PageUp)
const items = getMenuItems()
let items = getMenuItems()
assertMenuLinkedWithMenuItem(items[3])
})
)
@@ -2234,7 +2234,7 @@ describe('Keyboard interactions', () => {
// Open menu
await click(getMenuButton())
const items = getMenuItems()
let items = getMenuItems()
// We should be able to go to the second item
await type(word('bob'))
@@ -2270,7 +2270,7 @@ describe('Keyboard interactions', () => {
// Open menu
await press(Keys.ArrowUp)
const items = getMenuItems()
let items = getMenuItems()
// We should be on the last item
assertMenuLinkedWithMenuItem(items[2])
@@ -2309,7 +2309,7 @@ describe('Keyboard interactions', () => {
// Open menu
await press(Keys.ArrowUp)
const items = getMenuItems()
let items = getMenuItems()
// We should be on the last item
assertMenuLinkedWithMenuItem(items[2])
@@ -2350,7 +2350,7 @@ describe('Keyboard interactions', () => {
// Open menu
await press(Keys.ArrowUp)
const items = getMenuItems()
let items = getMenuItems()
// We should be on the last item
assertMenuLinkedWithMenuItem(items[2])
@@ -2398,7 +2398,7 @@ describe('Mouse interactions', () => {
assertMenuButtonLinkedWithMenu()
// Verify we have menu items
const items = getMenuItems()
let items = getMenuItems()
expect(items).toHaveLength(3)
items.forEach(item => assertMenuItem(item))
})
@@ -2601,7 +2601,7 @@ describe('Mouse interactions', () => {
</div>
)
const [button1, button2] = getMenuButtons()
let [button1, button2] = getMenuButtons()
// Click the first menu button
await click(button1)
@@ -2637,7 +2637,7 @@ describe('Mouse interactions', () => {
// Open menu
await click(getMenuButton())
const items = getMenuItems()
let items = getMenuItems()
// We should be able to go to the second item
await mouseMove(items[1])
assertMenuLinkedWithMenuItem(items[1])
@@ -2669,7 +2669,7 @@ describe('Mouse interactions', () => {
// Open menu
await click(getMenuButton())
const items = getMenuItems()
let items = getMenuItems()
// We should be able to go to the second item
await mouseMove(items[1])
assertMenuLinkedWithMenuItem(items[1])
@@ -2693,7 +2693,7 @@ describe('Mouse interactions', () => {
// Open menu
await click(getMenuButton())
const items = getMenuItems()
let items = getMenuItems()
// We should be able to go to the second item
await mouseMove(items[1])
@@ -2725,7 +2725,7 @@ describe('Mouse interactions', () => {
// Open menu
await click(getMenuButton())
const items = getMenuItems()
let items = getMenuItems()
await mouseMove(items[1])
assertNoActiveMenuItem()
@@ -2751,7 +2751,7 @@ describe('Mouse interactions', () => {
// Open menu
await click(getMenuButton())
const items = getMenuItems()
let items = getMenuItems()
// Try to hover over item 1, which is disabled
await mouseMove(items[1])
@@ -2778,7 +2778,7 @@ describe('Mouse interactions', () => {
// Open menu
await click(getMenuButton())
const items = getMenuItems()
let items = getMenuItems()
// We should be able to go to the second item
await mouseMove(items[1])
@@ -2822,7 +2822,7 @@ describe('Mouse interactions', () => {
// Open menu
await click(getMenuButton())
const items = getMenuItems()
let items = getMenuItems()
// Try to hover over item 1, which is disabled
await mouseMove(items[1])
@@ -2836,7 +2836,7 @@ describe('Mouse interactions', () => {
it(
'should be possible to click a menu item, which closes the menu',
suppressConsoleLogs(async () => {
const clickHandler = jest.fn()
let clickHandler = jest.fn()
render(
<Menu>
<Menu.Button>Trigger</Menu.Button>
@@ -2854,7 +2854,7 @@ describe('Mouse interactions', () => {
await click(getMenuButton())
assertMenu({ state: MenuState.Visible })
const items = getMenuItems()
let items = getMenuItems()
// We should be able to click the first item
await click(items[1])
@@ -2867,7 +2867,7 @@ describe('Mouse interactions', () => {
it(
'should be possible to click a menu item, which closes the menu and invokes the @click handler',
suppressConsoleLogs(async () => {
const clickHandler = jest.fn()
let clickHandler = jest.fn()
render(
<Menu>
<Menu.Button>Trigger</Menu.Button>
@@ -2926,7 +2926,7 @@ describe('Mouse interactions', () => {
await click(getMenuButton())
assertMenu({ state: MenuState.Visible })
const items = getMenuItems()
let items = getMenuItems()
// We should be able to click the first item
await click(items[1])
@@ -2952,7 +2952,7 @@ describe('Mouse interactions', () => {
await click(getMenuButton())
assertMenu({ state: MenuState.Visible })
const items = getMenuItems()
let items = getMenuItems()
// Verify that nothing is active yet
assertNoActiveMenuItem()
@@ -2983,7 +2983,7 @@ describe('Mouse interactions', () => {
await click(getMenuButton())
assertMenu({ state: MenuState.Visible })
const items = getMenuItems()
let items = getMenuItems()
// We should not be able to focus the first item
await focus(items[1])
@@ -2994,7 +2994,7 @@ describe('Mouse interactions', () => {
it(
'should not be possible to activate a disabled item',
suppressConsoleLogs(async () => {
const clickHandler = jest.fn()
let clickHandler = jest.fn()
render(
<Menu>
@@ -3017,7 +3017,7 @@ describe('Mouse interactions', () => {
await click(getMenuButton())
assertMenu({ state: MenuState.Visible })
const items = getMenuItems()
let items = getMenuItems()
await focus(items[0])
await click(items[1])
@@ -1,5 +1,23 @@
// WAI-ARIA: https://www.w3.org/TR/wai-aria-practices-1.2/#menubutton
import * as React from 'react'
import React, {
createContext,
createRef,
useCallback,
useContext,
useEffect,
useMemo,
useReducer,
useRef,
Fragment,
// Types
Dispatch,
ElementType,
KeyboardEvent as ReactKeyboardEvent,
MouseEvent as ReactMouseEvent,
MutableRefObject,
Ref,
} from 'react'
import { Props } from '../../types'
import { match } from '../../utils/match'
@@ -19,12 +37,12 @@ enum MenuStates {
Closed,
}
type MenuItemDataRef = React.MutableRefObject<{ textValue?: string; disabled: boolean }>
type MenuItemDataRef = MutableRefObject<{ textValue?: string; disabled: boolean }>
type StateDefinition = {
interface StateDefinition {
menuState: MenuStates
buttonRef: React.MutableRefObject<HTMLButtonElement | null>
itemsRef: React.MutableRefObject<HTMLDivElement | null>
buttonRef: MutableRefObject<HTMLButtonElement | null>
itemsRef: MutableRefObject<HTMLDivElement | null>
items: { id: string; dataRef: MenuItemDataRef }[]
searchQuery: string
activeItemIndex: number | null
@@ -52,7 +70,7 @@ type Actions =
| { type: ActionTypes.RegisterItem; id: string; dataRef: MenuItemDataRef }
| { type: ActionTypes.UnregisterItem; id: string }
const reducers: {
let reducers: {
[P in ActionTypes]: (
state: StateDefinition,
action: Extract<Actions, { type: P }>
@@ -65,7 +83,7 @@ const reducers: {
}),
[ActionTypes.OpenMenu]: state => ({ ...state, menuState: MenuStates.Open }),
[ActionTypes.GoToItem]: (state, action) => {
const activeItemIndex = calculateActiveIndex(action, {
let activeItemIndex = calculateActiveIndex(action, {
resolveItems: () => state.items,
resolveActiveIndex: () => state.activeItemIndex,
resolveId: item => item.id,
@@ -76,8 +94,8 @@ const reducers: {
return { ...state, searchQuery: '', activeItemIndex }
},
[ActionTypes.Search]: (state, action) => {
const searchQuery = state.searchQuery + action.value
const match = state.items.findIndex(
let searchQuery = state.searchQuery + action.value
let match = state.items.findIndex(
item =>
item.dataRef.current.textValue?.startsWith(searchQuery) && !item.dataRef.current.disabled
)
@@ -91,11 +109,10 @@ const reducers: {
items: [...state.items, { id: action.id, dataRef: action.dataRef }],
}),
[ActionTypes.UnregisterItem]: (state, action) => {
const nextItems = state.items.slice()
const currentActiveItem =
state.activeItemIndex !== null ? nextItems[state.activeItemIndex] : null
let nextItems = state.items.slice()
let currentActiveItem = state.activeItemIndex !== null ? nextItems[state.activeItemIndex] : null
const idx = nextItems.findIndex(a => a.id === action.id)
let idx = nextItems.findIndex(a => a.id === action.id)
if (idx !== -1) nextItems.splice(idx, 1)
@@ -114,13 +131,13 @@ const reducers: {
},
}
const MenuContext = React.createContext<[StateDefinition, React.Dispatch<Actions>] | null>(null)
let MenuContext = createContext<[StateDefinition, Dispatch<Actions>] | null>(null)
MenuContext.displayName = 'MenuContext'
function useMenuContext(component: string) {
const context = React.useContext(MenuContext)
let context = useContext(MenuContext)
if (context === null) {
const err = new Error(`<${component} /> is missing a parent <${Menu.name} /> component.`)
let err = new Error(`<${component} /> is missing a parent <${Menu.name} /> component.`)
if (Error.captureStackTrace) Error.captureStackTrace(err, useMenuContext)
throw err
}
@@ -133,26 +150,28 @@ function stateReducer(state: StateDefinition, action: Actions) {
// ---
const DEFAULT_MENU_TAG = React.Fragment
type MenuRenderPropArg = { open: boolean }
let DEFAULT_MENU_TAG = Fragment
interface MenuRenderPropArg {
open: boolean
}
export function Menu<TTag extends React.ElementType = typeof DEFAULT_MENU_TAG>(
export function Menu<TTag extends ElementType = typeof DEFAULT_MENU_TAG>(
props: Props<TTag, MenuRenderPropArg>
) {
const reducerBag = React.useReducer(stateReducer, {
let reducerBag = useReducer(stateReducer, {
menuState: MenuStates.Closed,
buttonRef: React.createRef(),
itemsRef: React.createRef(),
buttonRef: createRef(),
itemsRef: createRef(),
items: [],
searchQuery: '',
activeItemIndex: null,
} as StateDefinition)
const [{ menuState, itemsRef, buttonRef }, dispatch] = reducerBag
let [{ menuState, itemsRef, buttonRef }, dispatch] = reducerBag
React.useEffect(() => {
useEffect(() => {
function handler(event: MouseEvent) {
const target = event.target as HTMLElement
const active = document.activeElement
let target = event.target as HTMLElement
let active = document.activeElement
if (menuState !== MenuStates.Open) return
if (buttonRef.current?.contains(target)) return
@@ -166,7 +185,7 @@ export function Menu<TTag extends React.ElementType = typeof DEFAULT_MENU_TAG>(
return () => window.removeEventListener('mousedown', handler)
}, [menuState, itemsRef, buttonRef, dispatch])
const propsBag = React.useMemo(() => ({ open: menuState === MenuStates.Open }), [menuState])
let propsBag = useMemo(() => ({ open: menuState === MenuStates.Open }), [menuState])
return (
<MenuContext.Provider value={reducerBag}>
@@ -177,8 +196,10 @@ export function Menu<TTag extends React.ElementType = typeof DEFAULT_MENU_TAG>(
// ---
const DEFAULT_BUTTON_TAG = 'button'
type ButtonRenderPropArg = { open: boolean }
let DEFAULT_BUTTON_TAG = 'button' as const
interface ButtonRenderPropArg {
open: boolean
}
type ButtonPropsWeControl =
| 'id'
| 'type'
@@ -188,20 +209,18 @@ type ButtonPropsWeControl =
| 'onKeyDown'
| 'onClick'
const Button = forwardRefWithAs(function Button<
TTag extends React.ElementType = typeof DEFAULT_BUTTON_TAG
>(
let Button = forwardRefWithAs(function Button<TTag extends ElementType = typeof DEFAULT_BUTTON_TAG>(
props: Props<TTag, ButtonRenderPropArg, ButtonPropsWeControl>,
ref: React.Ref<HTMLButtonElement>
ref: Ref<HTMLButtonElement>
) {
const [state, dispatch] = useMenuContext([Menu.name, Button.name].join('.'))
const buttonRef = useSyncRefs(state.buttonRef, ref)
let [state, dispatch] = useMenuContext([Menu.name, Button.name].join('.'))
let buttonRef = useSyncRefs(state.buttonRef, ref)
const id = `headlessui-menu-button-${useId()}`
const d = useDisposables()
let id = `headlessui-menu-button-${useId()}`
let d = useDisposables()
const handleKeyDown = React.useCallback(
(event: React.KeyboardEvent<HTMLButtonElement>) => {
let handleKeyDown = useCallback(
(event: ReactKeyboardEvent<HTMLButtonElement>) => {
switch (event.key) {
// Ref: https://www.w3.org/TR/wai-aria-practices-1.2/#keyboard-interaction-13
@@ -229,8 +248,8 @@ const Button = forwardRefWithAs(function Button<
[dispatch, state, d]
)
const handleClick = React.useCallback(
(event: React.MouseEvent) => {
let handleClick = useCallback(
(event: ReactMouseEvent) => {
if (isDisabledReactIssue7711(event.currentTarget)) return event.preventDefault()
if (props.disabled) return
if (state.menuState === MenuStates.Open) {
@@ -245,9 +264,9 @@ const Button = forwardRefWithAs(function Button<
[dispatch, d, state, props.disabled]
)
const propsBag = React.useMemo(() => ({ open: state.menuState === MenuStates.Open }), [state])
const passthroughProps = props
const propsWeControl = {
let propsBag = useMemo(() => ({ open: state.menuState === MenuStates.Open }), [state])
let passthroughProps = props
let propsWeControl = {
ref: buttonRef,
id,
type: 'button',
@@ -263,8 +282,10 @@ const Button = forwardRefWithAs(function Button<
// ---
const DEFAULT_ITEMS_TAG = 'div'
type ItemsRenderPropArg = { open: boolean }
let DEFAULT_ITEMS_TAG = 'div' as const
interface ItemsRenderPropArg {
open: boolean
}
type ItemsPropsWeControl =
| 'aria-activedescendant'
| 'aria-labelledby'
@@ -273,23 +294,21 @@ type ItemsPropsWeControl =
| 'role'
| 'tabIndex'
const ItemsRenderFeatures = Features.RenderStrategy | Features.Static
let ItemsRenderFeatures = Features.RenderStrategy | Features.Static
const Items = forwardRefWithAs(function Items<
TTag extends React.ElementType = typeof DEFAULT_ITEMS_TAG
>(
let Items = forwardRefWithAs(function Items<TTag extends ElementType = typeof DEFAULT_ITEMS_TAG>(
props: Props<TTag, ItemsRenderPropArg, ItemsPropsWeControl> &
PropsForFeatures<typeof ItemsRenderFeatures>,
ref: React.Ref<HTMLDivElement>
ref: Ref<HTMLDivElement>
) {
const [state, dispatch] = useMenuContext([Menu.name, Items.name].join('.'))
const itemsRef = useSyncRefs(state.itemsRef, ref)
let [state, dispatch] = useMenuContext([Menu.name, Items.name].join('.'))
let itemsRef = useSyncRefs(state.itemsRef, ref)
const id = `headlessui-menu-items-${useId()}`
const searchDisposables = useDisposables()
let id = `headlessui-menu-items-${useId()}`
let searchDisposables = useDisposables()
const handleKeyDown = React.useCallback(
(event: React.KeyboardEvent<HTMLDivElement>) => {
let handleKeyDown = useCallback(
(event: ReactKeyboardEvent<HTMLDivElement>) => {
searchDisposables.dispose()
switch (event.key) {
@@ -306,7 +325,7 @@ const Items = forwardRefWithAs(function Items<
event.preventDefault()
dispatch({ type: ActionTypes.CloseMenu })
if (state.activeItemIndex !== null) {
const { id } = state.items[state.activeItemIndex]
let { id } = state.items[state.activeItemIndex]
document.getElementById(id)?.click()
}
disposables().nextFrame(() => state.buttonRef.current?.focus({ preventScroll: true }))
@@ -350,8 +369,8 @@ const Items = forwardRefWithAs(function Items<
[dispatch, searchDisposables, state]
)
const propsBag = React.useMemo(() => ({ open: state.menuState === MenuStates.Open }), [state])
const propsWeControl = {
let propsBag = useMemo(() => ({ open: state.menuState === MenuStates.Open }), [state])
let propsWeControl = {
'aria-activedescendant':
state.activeItemIndex === null ? undefined : state.items[state.activeItemIndex]?.id,
'aria-labelledby': state.buttonRef.current?.id,
@@ -361,7 +380,7 @@ const Items = forwardRefWithAs(function Items<
tabIndex: 0,
ref: itemsRef,
}
const passthroughProps = props
let passthroughProps = props
return render(
{ ...passthroughProps, ...propsWeControl },
@@ -374,8 +393,11 @@ const Items = forwardRefWithAs(function Items<
// ---
const DEFAULT_ITEM_TAG = React.Fragment
type ItemRenderPropArg = { active: boolean; disabled: boolean }
let DEFAULT_ITEM_TAG = Fragment
interface ItemRenderPropArg {
active: boolean
disabled: boolean
}
type MenuItemPropsWeControl =
| 'id'
| 'role'
@@ -387,7 +409,7 @@ type MenuItemPropsWeControl =
| 'onMouseMove'
| 'onFocus'
function Item<TTag extends React.ElementType = typeof DEFAULT_ITEM_TAG>(
function Item<TTag extends ElementType = typeof DEFAULT_ITEM_TAG>(
props: Props<TTag, ItemRenderPropArg, MenuItemPropsWeControl | 'className'> & {
disabled?: boolean
onClick?: (event: { preventDefault: Function }) => void
@@ -396,13 +418,12 @@ function Item<TTag extends React.ElementType = typeof DEFAULT_ITEM_TAG>(
className?: ((bag: ItemRenderPropArg) => string) | string
}
) {
const { disabled = false, className, onClick, ...passthroughProps } = props
const [state, dispatch] = useMenuContext([Menu.name, Item.name].join('.'))
const id = `headlessui-menu-item-${useId()}`
const active =
state.activeItemIndex !== null ? state.items[state.activeItemIndex].id === id : false
let { disabled = false, className, onClick, ...passthroughProps } = props
let [state, dispatch] = useMenuContext([Menu.name, Item.name].join('.'))
let id = `headlessui-menu-item-${useId()}`
let active = state.activeItemIndex !== null ? state.items[state.activeItemIndex].id === id : false
const bag = React.useRef<MenuItemDataRef['current']>({ disabled })
let bag = useRef<MenuItemDataRef['current']>({ disabled })
useIsoMorphicEffect(() => {
bag.current.disabled = disabled
@@ -417,8 +438,8 @@ function Item<TTag extends React.ElementType = typeof DEFAULT_ITEM_TAG>(
return () => dispatch({ type: ActionTypes.UnregisterItem, id })
}, [bag, id])
const handleClick = React.useCallback(
(event: React.MouseEvent) => {
let handleClick = useCallback(
(event: MouseEvent) => {
if (disabled) return event.preventDefault()
dispatch({ type: ActionTypes.CloseMenu })
disposables().nextFrame(() => state.buttonRef.current?.focus({ preventScroll: true }))
@@ -427,25 +448,25 @@ function Item<TTag extends React.ElementType = typeof DEFAULT_ITEM_TAG>(
[dispatch, state.buttonRef, disabled, onClick]
)
const handleFocus = React.useCallback(() => {
let handleFocus = useCallback(() => {
if (disabled) return dispatch({ type: ActionTypes.GoToItem, focus: Focus.Nothing })
dispatch({ type: ActionTypes.GoToItem, focus: Focus.Specific, id })
}, [disabled, id, dispatch])
const handleMove = React.useCallback(() => {
let handleMove = useCallback(() => {
if (disabled) return
if (active) return
dispatch({ type: ActionTypes.GoToItem, focus: Focus.Specific, id })
}, [disabled, active, id, dispatch])
const handleLeave = React.useCallback(() => {
let handleLeave = useCallback(() => {
if (disabled) return
if (!active) return
dispatch({ type: ActionTypes.GoToItem, focus: Focus.Nothing })
}, [disabled, active, dispatch])
const propsBag = React.useMemo(() => ({ active, disabled }), [active, disabled])
const propsWeControl = {
let propsBag = useMemo(() => ({ active, disabled }), [active, disabled])
let propsWeControl = {
id,
role: 'menuitem',
tabIndex: -1,
@@ -1,4 +1,4 @@
import React from 'react'
import React, { createElement, useState } from 'react'
import { render } from '@testing-library/react'
import { Switch } from './switch'
@@ -18,7 +18,7 @@ describe('Safe guards', () => {
it.each([['Switch.Label', Switch.Label]])(
'should error when we are using a <%s /> without a parent <Switch.Group />',
suppressConsoleLogs((name, Component) => {
expect(() => render(React.createElement(Component))).toThrowError(
expect(() => render(createElement(Component))).toThrowError(
`<${name} /> is missing a parent <Switch.Group /> component.`
)
})
@@ -120,9 +120,9 @@ describe('Render composition', () => {
describe('Keyboard interactions', () => {
describe('`Space` key', () => {
it('should be possible to toggle the Switch with Space', async () => {
const handleChange = jest.fn()
let handleChange = jest.fn()
function Example() {
const [state, setState] = React.useState(false)
let [state, setState] = useState(false)
return (
<Switch
checked={state}
@@ -158,7 +158,7 @@ describe('Keyboard interactions', () => {
describe('`Enter` key', () => {
it('should not be possible to use Enter to toggle the Switch', async () => {
const handleChange = jest.fn()
let handleChange = jest.fn()
render(<Switch checked={false} onChange={handleChange} />)
// Ensure checkbox is off
@@ -203,9 +203,9 @@ describe('Keyboard interactions', () => {
describe('Mouse interactions', () => {
it('should be possible to toggle the Switch with a click', async () => {
const handleChange = jest.fn()
let handleChange = jest.fn()
function Example() {
const [state, setState] = React.useState(false)
let [state, setState] = useState(false)
return (
<Switch
checked={state}
@@ -236,9 +236,9 @@ describe('Mouse interactions', () => {
})
it('should be possible to toggle the Switch with a click on the Label', async () => {
const handleChange = jest.fn()
let handleChange = jest.fn()
function Example() {
const [state, setState] = React.useState(false)
let [state, setState] = useState(false)
return (
<Switch.Group>
<Switch
@@ -1,4 +1,16 @@
import * as React from 'react'
import React, {
createContext,
useCallback,
useContext,
useMemo,
useState,
Fragment,
// Types
ElementType,
KeyboardEvent as ReactKeyboardEvent,
MouseEvent as ReactMouseEvent,
} from 'react'
import { Props } from '../../types'
import { render } from '../../utils/render'
@@ -7,7 +19,7 @@ import { Keys } from '../keyboard'
import { resolvePropValue } from '../../utils/resolve-prop-value'
import { isDisabledReactIssue7711 } from '../../utils/bugs'
type StateDefinition = {
interface StateDefinition {
switch: HTMLButtonElement | null
label: HTMLLabelElement | null
@@ -15,13 +27,13 @@ type StateDefinition = {
setLabel(element: HTMLLabelElement): void
}
const GroupContext = React.createContext<StateDefinition | null>(null)
let GroupContext = createContext<StateDefinition | null>(null)
GroupContext.displayName = 'GroupContext'
function useGroupContext(component: string) {
const context = React.useContext(GroupContext)
let context = useContext(GroupContext)
if (context === null) {
const err = new Error(`<${component} /> is missing a parent <Switch.Group /> component.`)
let err = new Error(`<${component} /> is missing a parent <Switch.Group /> component.`)
if (Error.captureStackTrace) Error.captureStackTrace(err, useGroupContext)
throw err
}
@@ -30,13 +42,13 @@ function useGroupContext(component: string) {
// ---
const DEFAULT_GROUP_TAG = React.Fragment
let DEFAULT_GROUP_TAG = Fragment
function Group<TTag extends React.ElementType = typeof DEFAULT_GROUP_TAG>(props: Props<TTag>) {
const [switchElement, setSwitchElement] = React.useState<HTMLButtonElement | null>(null)
const [labelElement, setLabelElement] = React.useState<HTMLLabelElement | null>(null)
function Group<TTag extends ElementType = typeof DEFAULT_GROUP_TAG>(props: Props<TTag>) {
let [switchElement, setSwitchElement] = useState<HTMLButtonElement | null>(null)
let [labelElement, setLabelElement] = useState<HTMLLabelElement | null>(null)
const context = React.useMemo<StateDefinition>(
let context = useMemo<StateDefinition>(
() => ({
switch: switchElement,
label: labelElement,
@@ -45,6 +57,7 @@ function Group<TTag extends React.ElementType = typeof DEFAULT_GROUP_TAG>(props:
}),
[switchElement, setSwitchElement, labelElement, setLabelElement]
)
return (
<GroupContext.Provider value={context}>
{render(props, {}, DEFAULT_GROUP_TAG)}
@@ -54,8 +67,10 @@ function Group<TTag extends React.ElementType = typeof DEFAULT_GROUP_TAG>(props:
// ---
const DEFAULT_SWITCH_TAG = 'button'
type SwitchRenderPropArg = { checked: boolean }
let DEFAULT_SWITCH_TAG = 'button' as const
interface SwitchRenderPropArg {
checked: boolean
}
type SwitchPropsWeControl =
| 'id'
| 'role'
@@ -65,7 +80,7 @@ type SwitchPropsWeControl =
| 'onKeyUp'
| 'onKeyPress'
export function Switch<TTag extends React.ElementType = typeof DEFAULT_SWITCH_TAG>(
export function Switch<TTag extends ElementType = typeof DEFAULT_SWITCH_TAG>(
props: Props<
TTag,
SwitchRenderPropArg,
@@ -78,21 +93,21 @@ export function Switch<TTag extends React.ElementType = typeof DEFAULT_SWITCH_TA
className?: ((bag: SwitchRenderPropArg) => string) | string
}
) {
const { checked, onChange, className, ...passThroughProps } = props
const id = `headlessui-switch-${useId()}`
const groupContext = React.useContext(GroupContext)
let { checked, onChange, className, ...passThroughProps } = props
let id = `headlessui-switch-${useId()}`
let groupContext = useContext(GroupContext)
const toggle = React.useCallback(() => onChange(!checked), [onChange, checked])
const handleClick = React.useCallback(
(event: React.MouseEvent) => {
let toggle = useCallback(() => onChange(!checked), [onChange, checked])
let handleClick = useCallback(
(event: ReactMouseEvent) => {
if (isDisabledReactIssue7711(event.currentTarget)) return event.preventDefault()
event.preventDefault()
toggle()
},
[toggle]
)
const handleKeyUp = React.useCallback(
(event: React.KeyboardEvent<HTMLElement>) => {
let handleKeyUp = useCallback(
(event: ReactKeyboardEvent<HTMLElement>) => {
if (event.key !== Keys.Tab) event.preventDefault()
if (event.key === Keys.Space) toggle()
},
@@ -100,13 +115,13 @@ export function Switch<TTag extends React.ElementType = typeof DEFAULT_SWITCH_TA
)
// This is needed so that we can "cancel" the click event when we use the `Enter` key on a button.
const handleKeyPress = React.useCallback(
(event: React.KeyboardEvent<HTMLElement>) => event.preventDefault(),
let handleKeyPress = useCallback(
(event: ReactKeyboardEvent<HTMLElement>) => event.preventDefault(),
[]
)
const propsBag = React.useMemo<SwitchRenderPropArg>(() => ({ checked }), [checked])
const propsWeControl = {
let propsBag = useMemo<SwitchRenderPropArg>(() => ({ checked }), [checked])
let propsWeControl = {
id,
ref: groupContext === null ? undefined : groupContext.setSwitch,
role: 'switch',
@@ -128,23 +143,23 @@ export function Switch<TTag extends React.ElementType = typeof DEFAULT_SWITCH_TA
// ---
const DEFAULT_LABEL_TAG = 'label'
type LabelRenderPropArg = {}
let DEFAULT_LABEL_TAG = 'label' as const
interface LabelRenderPropArg {}
type LabelPropsWeControl = 'id' | 'ref' | 'onClick'
function Label<TTag extends React.ElementType = typeof DEFAULT_LABEL_TAG>(
function Label<TTag extends ElementType = typeof DEFAULT_LABEL_TAG>(
props: Props<TTag, LabelRenderPropArg, LabelPropsWeControl>
) {
const state = useGroupContext([Switch.name, Label.name].join('.'))
const id = `headlessui-switch-label-${useId()}`
let state = useGroupContext([Switch.name, Label.name].join('.'))
let id = `headlessui-switch-label-${useId()}`
const handleClick = React.useCallback(() => {
let handleClick = useCallback(() => {
if (!state.switch) return
state.switch.click()
state.switch.focus({ preventScroll: true })
}, [state.switch])
const propsWeControl = { ref: state.setLabel, id, onClick: handleClick }
let propsWeControl = { ref: state.setLabel, id, onClick: handleClick }
return render({ ...props, ...propsWeControl }, {}, DEFAULT_LABEL_TAG)
}
@@ -1,4 +1,4 @@
import * as React from 'react'
import React, { Fragment, useState, useRef, useLayoutEffect } from 'react'
import { render, fireEvent } from '@testing-library/react'
import { suppressConsoleLogs } from '../../test-utils/suppress-console-logs'
@@ -41,7 +41,7 @@ it(
describe('Setup API', () => {
describe('shallow', () => {
it('should render a div and its children by default', () => {
const { container } = render(<Transition show={true}>Children</Transition>)
let { container } = render(<Transition show={true}>Children</Transition>)
expect(container.firstChild).toMatchInlineSnapshot(`
<div>
@@ -51,7 +51,7 @@ describe('Setup API', () => {
})
it('should passthrough all the props (that we do not use internally)', () => {
const { container } = render(
let { container } = render(
<Transition show={true} id="root" className="text-blue-400">
Children
</Transition>
@@ -68,7 +68,7 @@ describe('Setup API', () => {
})
it('should render another component if the `as` prop is used and its children by default', () => {
const { container } = render(
let { container } = render(
<Transition show={true} as="a">
Children
</Transition>
@@ -82,7 +82,7 @@ describe('Setup API', () => {
})
it('should passthrough all the props (that we do not use internally) even when using an `as` prop', () => {
const { container } = render(
let { container } = render(
<Transition show={true} as="a" href="/" className="text-blue-400">
Children
</Transition>
@@ -99,13 +99,13 @@ describe('Setup API', () => {
})
it('should render nothing when the show prop is false', () => {
const { container } = render(<Transition show={false}>Children</Transition>)
let { container } = render(<Transition show={false}>Children</Transition>)
expect(container.firstChild).toMatchInlineSnapshot(`null`)
})
it('should be possible to change the underlying DOM tag', () => {
const { container } = render(
let { container } = render(
<Transition show={true} as="a">
Children
</Transition>
@@ -119,8 +119,8 @@ describe('Setup API', () => {
})
it('should be possible to use a render prop', () => {
const { container } = render(
<Transition show={true} as={React.Fragment}>
let { container } = render(
<Transition show={true} as={Fragment}>
{() => <span>Children</span>}
</Transition>
)
@@ -143,7 +143,7 @@ describe('Setup API', () => {
expect(() => {
render(
<Transition show={true} as={React.Fragment}>
<Transition show={true} as={Fragment}>
{() => <Dummy />}
</Transition>
)
@@ -182,7 +182,7 @@ describe('Setup API', () => {
})
it('should be possible to nest transition components', () => {
const { container } = render(
let { container } = render(
<div className="My Page">
<Transition show={true}>
<Transition.Child>Sidebar</Transition.Child>
@@ -208,7 +208,7 @@ describe('Setup API', () => {
})
it('should be possible to change the underlying DOM tag of the Transition.Child components', () => {
const { container } = render(
let { container } = render(
<div className="My Page">
<Transition show={true}>
<Transition.Child as="aside">Sidebar</Transition.Child>
@@ -234,7 +234,7 @@ describe('Setup API', () => {
})
it('should be possible to change the underlying DOM tag of the Transition component and Transition.Child components', () => {
const { container } = render(
let { container } = render(
<div className="My Page">
<Transition show={true} as="article">
<Transition.Child as="aside">Sidebar</Transition.Child>
@@ -260,13 +260,11 @@ describe('Setup API', () => {
})
it('should be possible to use render props on the Transition.Child components', () => {
const { container } = render(
let { container } = render(
<div className="My Page">
<Transition show={true}>
<Transition.Child as={React.Fragment}>{() => <aside>Sidebar</aside>}</Transition.Child>
<Transition.Child as={React.Fragment}>
{() => <section>Content</section>}
</Transition.Child>
<Transition.Child as={Fragment}>{() => <aside>Sidebar</aside>}</Transition.Child>
<Transition.Child as={Fragment}>{() => <section>Content</section>}</Transition.Child>
</Transition>
</div>
)
@@ -288,15 +286,13 @@ describe('Setup API', () => {
})
it('should be possible to use render props on the Transition and Transition.Child components', () => {
const { container } = render(
let { container } = render(
<div className="My Page">
<Transition show={true} as={React.Fragment}>
<Transition show={true} as={Fragment}>
{() => (
<article>
<Transition.Child as={React.Fragment}>
{() => <aside>Sidebar</aside>}
</Transition.Child>
<Transition.Child as={React.Fragment}>
<Transition.Child as={Fragment}>{() => <aside>Sidebar</aside>}</Transition.Child>
<Transition.Child as={Fragment}>
{() => <section>Content</section>}
</Transition.Child>
</article>
@@ -334,12 +330,8 @@ describe('Setup API', () => {
render(
<div className="My Page">
<Transition show={true}>
<Transition.Child as={React.Fragment}>
{() => <Dummy>Sidebar</Dummy>}
</Transition.Child>
<Transition.Child as={React.Fragment}>
{() => <Dummy>Content</Dummy>}
</Transition.Child>
<Transition.Child as={Fragment}>{() => <Dummy>Sidebar</Dummy>}</Transition.Child>
<Transition.Child as={Fragment}>{() => <Dummy>Content</Dummy>}</Transition.Child>
</Transition>
</div>
)
@@ -361,7 +353,7 @@ describe('Setup API', () => {
expect(() => {
render(
<div className="My Page">
<Transition show={true} as={React.Fragment}>
<Transition show={true} as={Fragment}>
{() => (
<Dummy>
<Transition.Child>{() => <aside>Sidebar</aside>}</Transition.Child>
@@ -380,7 +372,7 @@ describe('Setup API', () => {
describe('transition classes', () => {
it('should be possible to passthrough the transition classes', () => {
const { container } = render(
let { container } = render(
<Transition
show={true}
enter="enter"
@@ -402,7 +394,7 @@ describe('Setup API', () => {
})
it('should be possible to passthrough the transition classes and immediately apply the enter transitions when appear is set to true', () => {
const { container } = render(
let { container } = render(
<Transition
show={true}
appear={true}
@@ -431,10 +423,10 @@ describe('Setup API', () => {
describe('Transitions', () => {
describe('shallow transitions', () => {
it('should transition in completely (duration defined in milliseconds)', async () => {
const enterDuration = 50
let enterDuration = 50
function Example() {
const [show, setShow] = React.useState(false)
let [show, setShow] = useState(false)
return (
<>
@@ -451,7 +443,7 @@ describe('Transitions', () => {
)
}
const timeline = await executeTimeline(<Example />, [
let timeline = await executeTimeline(<Example />, [
// Toggle to show
({ getByTestId }) => {
fireEvent.click(getByTestId('toggle'))
@@ -480,10 +472,10 @@ describe('Transitions', () => {
})
it('should transition in completely (duration defined in seconds)', async () => {
const enterDuration = 50
let enterDuration = 50
function Example() {
const [show, setShow] = React.useState(false)
let [show, setShow] = useState(false)
return (
<>
@@ -501,7 +493,7 @@ describe('Transitions', () => {
)
}
const timeline = await executeTimeline(<Example />, [
let timeline = await executeTimeline(<Example />, [
// Toggle to show
({ getByTestId }) => {
fireEvent.click(getByTestId('toggle'))
@@ -530,10 +522,10 @@ describe('Transitions', () => {
})
it('should transition in completely (duration defined in seconds) in (render strategy = hidden)', async () => {
const enterDuration = 50
let enterDuration = 50
function Example() {
const [show, setShow] = React.useState(false)
let [show, setShow] = useState(false)
return (
<>
@@ -551,7 +543,7 @@ describe('Transitions', () => {
)
}
const timeline = await executeTimeline(<Example />, [
let timeline = await executeTimeline(<Example />, [
// Toggle to show
({ getByTestId }) => {
fireEvent.click(getByTestId('toggle'))
@@ -577,10 +569,10 @@ describe('Transitions', () => {
})
it('should transition in completely', async () => {
const enterDuration = 50
let enterDuration = 50
function Example() {
const [show, setShow] = React.useState(false)
let [show, setShow] = useState(false)
return (
<>
@@ -597,7 +589,7 @@ describe('Transitions', () => {
)
}
const timeline = await executeTimeline(<Example />, [
let timeline = await executeTimeline(<Example />, [
// Toggle to show
({ getByTestId }) => {
fireEvent.click(getByTestId('toggle'))
@@ -628,10 +620,10 @@ describe('Transitions', () => {
it(
'should transition out completely',
suppressConsoleLogs(async () => {
const leaveDuration = 50
let leaveDuration = 50
function Example() {
const [show, setShow] = React.useState(true)
let [show, setShow] = useState(true)
return (
<>
@@ -648,7 +640,7 @@ describe('Transitions', () => {
)
}
const timeline = await executeTimeline(<Example />, [
let timeline = await executeTimeline(<Example />, [
// Toggle to hide
({ getByTestId }) => {
fireEvent.click(getByTestId('toggle'))
@@ -682,10 +674,10 @@ describe('Transitions', () => {
it(
'should transition out completely (render strategy = hidden)',
suppressConsoleLogs(async () => {
const leaveDuration = 50
let leaveDuration = 50
function Example() {
const [show, setShow] = React.useState(true)
let [show, setShow] = useState(true)
return (
<>
@@ -702,7 +694,7 @@ describe('Transitions', () => {
)
}
const timeline = await executeTimeline(<Example />, [
let timeline = await executeTimeline(<Example />, [
// Toggle to hide
({ getByTestId }) => {
fireEvent.click(getByTestId('toggle'))
@@ -733,11 +725,11 @@ describe('Transitions', () => {
it(
'should transition in and out completely',
suppressConsoleLogs(async () => {
const enterDuration = 50
const leaveDuration = 75
let enterDuration = 50
let leaveDuration = 75
function Example() {
const [show, setShow] = React.useState(false)
let [show, setShow] = useState(false)
return (
<>
@@ -763,7 +755,7 @@ describe('Transitions', () => {
)
}
const timeline = await executeTimeline(<Example />, [
let timeline = await executeTimeline(<Example />, [
// Toggle to show
({ getByTestId }) => {
fireEvent.click(getByTestId('toggle'))
@@ -818,11 +810,11 @@ describe('Transitions', () => {
it(
'should transition in and out completely (render strategy = hidden)',
suppressConsoleLogs(async () => {
const enterDuration = 50
const leaveDuration = 75
let enterDuration = 50
let leaveDuration = 75
function Example() {
const [show, setShow] = React.useState(false)
let [show, setShow] = useState(false)
return (
<>
@@ -849,7 +841,7 @@ describe('Transitions', () => {
)
}
const timeline = await executeTimeline(<Example />, [
let timeline = await executeTimeline(<Example />, [
// Toggle to show
({ getByTestId }) => {
fireEvent.click(getByTestId('toggle'))
@@ -922,11 +914,11 @@ describe('Transitions', () => {
it(
'should not unmount the whole tree when some children are still transitioning',
suppressConsoleLogs(async () => {
const slowLeaveDuration = 150
const fastLeaveDuration = 50
let slowLeaveDuration = 150
let fastLeaveDuration = 50
function Example() {
const [show, setShow] = React.useState(true)
let [show, setShow] = useState(true)
return (
<>
@@ -949,7 +941,7 @@ describe('Transitions', () => {
)
}
const timeline = await executeTimeline(<Example />, [
let timeline = await executeTimeline(<Example />, [
// Toggle to hide
({ getByTestId }) => {
fireEvent.click(getByTestId('toggle'))
@@ -1003,11 +995,11 @@ describe('Transitions', () => {
it(
'should not unmount the whole tree when some children are still transitioning',
suppressConsoleLogs(async () => {
const slowLeaveDuration = 150
const fastLeaveDuration = 50
let slowLeaveDuration = 150
let fastLeaveDuration = 50
function Example() {
const [show, setShow] = React.useState(true)
let [show, setShow] = useState(true)
return (
<>
@@ -1033,7 +1025,7 @@ describe('Transitions', () => {
)
}
const timeline = await executeTimeline(<Example />, [
let timeline = await executeTimeline(<Example />, [
// Toggle to hide
({ getByTestId }) => {
fireEvent.click(getByTestId('toggle'))
@@ -1102,15 +1094,15 @@ describe('Events', () => {
it(
'should fire events for all the stages',
suppressConsoleLogs(async () => {
const eventHandler = jest.fn()
const enterDuration = 50
const leaveDuration = 75
let eventHandler = jest.fn()
let enterDuration = 50
let leaveDuration = 75
function Example() {
const [show, setShow] = React.useState(false)
const start = React.useRef(Date.now())
let [show, setShow] = useState(false)
let start = useRef(Date.now())
React.useLayoutEffect(() => {
useLayoutEffect(() => {
start.current = Date.now()
}, [])
@@ -1144,7 +1136,7 @@ describe('Events', () => {
)
}
const timeline = await executeTimeline(<Example />, [
let timeline = await executeTimeline(<Example />, [
// Toggle to show
({ getByTestId }) => {
fireEvent.click(getByTestId('toggle'))
@@ -1202,11 +1194,11 @@ describe('Events', () => {
'afterLeave',
])
const enterHookDiff = eventHandler.mock.calls[1][1] - eventHandler.mock.calls[0][1]
let enterHookDiff = eventHandler.mock.calls[1][1] - eventHandler.mock.calls[0][1]
expect(enterHookDiff).toBeGreaterThanOrEqual(enterDuration)
expect(enterHookDiff).toBeLessThanOrEqual(enterDuration * 2)
const leaveHookDiff = eventHandler.mock.calls[3][1] - eventHandler.mock.calls[2][1]
let leaveHookDiff = eventHandler.mock.calls[3][1] - eventHandler.mock.calls[2][1]
expect(leaveHookDiff).toBeGreaterThanOrEqual(leaveDuration)
expect(leaveHookDiff).toBeLessThanOrEqual(leaveDuration * 2)
})
@@ -1,5 +1,18 @@
import * as React from 'react'
import { Props } from 'types'
import React, {
useMemo,
createContext,
useContext,
useRef,
useEffect,
useCallback,
useState,
Fragment,
// Types
ElementType,
MutableRefObject,
} from 'react'
import { Props, Expand } from 'types'
import { useId } from '../../hooks/use-id'
import { useIsInitialRender } from '../../hooks/use-is-initial-render'
@@ -13,16 +26,16 @@ import { Reason, transition } from './utils/transition'
type ID = ReturnType<typeof useId>
function useSplitClasses(classes: string = '') {
return React.useMemo(() => classes.split(' ').filter(className => className.trim().length > 1), [
return useMemo(() => classes.split(' ').filter(className => className.trim().length > 1), [
classes,
])
}
type TransitionContextValues = {
interface TransitionContextValues {
show: boolean
appear: boolean
} | null
const TransitionContext = React.createContext<TransitionContextValues>(null)
}
let TransitionContext = createContext<TransitionContextValues | null>(null)
TransitionContext.displayName = 'TransitionContext'
enum TreeStates {
@@ -30,28 +43,29 @@ enum TreeStates {
Hidden = 'hidden',
}
export type TransitionClasses = Partial<{
enter: string
enterFrom: string
enterTo: string
leave: string
leaveFrom: string
leaveTo: string
}>
export interface TransitionClasses {
enter?: string
enterFrom?: string
enterTo?: string
leave?: string
leaveFrom?: string
leaveTo?: string
}
export type TransitionEvents = Partial<{
beforeEnter(): void
afterEnter(): void
beforeLeave(): void
afterLeave(): void
}>
export interface TransitionEvents {
beforeEnter?: () => void
afterEnter?: () => void
beforeLeave?: () => void
afterLeave?: () => void
}
type TransitionChildProps<TTag> = Props<TTag, TransitionChildRenderPropArg> &
PropsForFeatures<typeof TransitionChildRenderFeatures> &
Partial<{ appear: boolean } & TransitionClasses & TransitionEvents>
TransitionClasses &
TransitionEvents & { appear?: boolean }
function useTransitionContext() {
const context = React.useContext(TransitionContext)
let context = useContext(TransitionContext)
if (context === null) {
throw new Error('A <Transition.Child /> is used but it is missing a parent <Transition />.')
@@ -61,7 +75,7 @@ function useTransitionContext() {
}
function useParentNesting() {
const context = React.useContext(NestingContext)
let context = useContext(NestingContext)
if (context === null) {
throw new Error('A <Transition.Child /> is used but it is missing a parent <Transition />.')
@@ -70,13 +84,13 @@ function useParentNesting() {
return context
}
type NestingContextValues = {
children: React.MutableRefObject<{ id: ID; state: TreeStates }[]>
interface NestingContextValues {
children: MutableRefObject<{ id: ID; state: TreeStates }[]>
register: (id: ID) => () => void
unregister: (id: ID, strategy?: RenderStrategy) => void
}
const NestingContext = React.createContext<NestingContextValues | null>(null)
let NestingContext = createContext<NestingContextValues | null>(null)
NestingContext.displayName = 'NestingContext'
function hasChildren(
@@ -87,17 +101,17 @@ function hasChildren(
}
function useNesting(done?: () => void) {
const doneRef = React.useRef(done)
const transitionableChildren = React.useRef<NestingContextValues['children']['current']>([])
const mounted = useIsMounted()
let doneRef = useRef(done)
let transitionableChildren = useRef<NestingContextValues['children']['current']>([])
let mounted = useIsMounted()
React.useEffect(() => {
useEffect(() => {
doneRef.current = done
}, [done])
const unregister = React.useCallback(
let unregister = useCallback(
(childId: ID, strategy = RenderStrategy.Hidden) => {
const idx = transitionableChildren.current.findIndex(({ id }) => id === childId)
let idx = transitionableChildren.current.findIndex(({ id }) => id === childId)
if (idx === -1) return
match(strategy, {
@@ -116,9 +130,9 @@ function useNesting(done?: () => void) {
[doneRef, mounted, transitionableChildren]
)
const register = React.useCallback(
let register = useCallback(
(childId: ID) => {
const child = transitionableChildren.current.find(({ id }) => id === childId)
let child = transitionableChildren.current.find(({ id }) => id === childId)
if (!child) {
transitionableChildren.current.push({ id: childId, state: TreeStates.Visible })
} else if (child.state !== TreeStates.Visible) {
@@ -130,7 +144,7 @@ function useNesting(done?: () => void) {
[transitionableChildren, unregister]
)
return React.useMemo(
return useMemo(
() => ({
children: transitionableChildren,
register,
@@ -141,7 +155,7 @@ function useNesting(done?: () => void) {
}
function noop() {}
const eventNames: (keyof TransitionEvents)[] = [
let eventNames: (keyof TransitionEvents)[] = [
'beforeEnter',
'afterEnter',
'beforeLeave',
@@ -155,9 +169,9 @@ function ensureEventHooksExist(events: TransitionEvents) {
}
function useEvents(events: TransitionEvents) {
const eventsRef = React.useRef(ensureEventHooksExist(events))
let eventsRef = useRef(ensureEventHooksExist(events))
React.useEffect(() => {
useEffect(() => {
eventsRef.current = ensureEventHooksExist(events)
}, [events])
@@ -166,14 +180,14 @@ function useEvents(events: TransitionEvents) {
// ---
const DEFAULT_TRANSITION_CHILD_TAG = 'div'
type TransitionChildRenderPropArg = React.MutableRefObject<HTMLDivElement>
const TransitionChildRenderFeatures = Features.RenderStrategy
let DEFAULT_TRANSITION_CHILD_TAG = 'div' as const
type TransitionChildRenderPropArg = MutableRefObject<HTMLDivElement>
let TransitionChildRenderFeatures = Features.RenderStrategy
function TransitionChild<TTag extends React.ElementType = typeof DEFAULT_TRANSITION_CHILD_TAG>(
function TransitionChild<TTag extends ElementType = typeof DEFAULT_TRANSITION_CHILD_TAG>(
props: TransitionChildProps<TTag>
) {
const {
let {
// Event "handlers"
beforeEnter,
afterEnter,
@@ -188,20 +202,20 @@ function TransitionChild<TTag extends React.ElementType = typeof DEFAULT_TRANSIT
leaveFrom,
leaveTo,
...rest
} = props
const container = React.useRef<HTMLElement | null>(null)
const [state, setState] = React.useState(TreeStates.Visible)
const strategy = rest.unmount ? RenderStrategy.Unmount : RenderStrategy.Hidden
} = props as Expand<typeof props>
let container = useRef<HTMLElement | null>(null)
let [state, setState] = useState(TreeStates.Visible)
let strategy = rest.unmount ? RenderStrategy.Unmount : RenderStrategy.Hidden
const { show, appear } = useTransitionContext()
const { register, unregister } = useParentNesting()
let { show, appear } = useTransitionContext()
let { register, unregister } = useParentNesting()
const initial = useIsInitialRender()
const id = useId()
let initial = useIsInitialRender()
let id = useId()
const isTransitioning = React.useRef(false)
let isTransitioning = useRef(false)
const nesting = useNesting(() => {
let nesting = useNesting(() => {
// When all children have been unmounted we can only hide ourselves if and only if we are not
// transitioning ourserlves. Otherwise we would unmount before the transitions are finished.
if (!isTransitioning.current) {
@@ -233,27 +247,27 @@ function TransitionChild<TTag extends React.ElementType = typeof DEFAULT_TRANSIT
})
}, [state, id, register, unregister, show, strategy])
const enterClasses = useSplitClasses(enter)
const enterFromClasses = useSplitClasses(enterFrom)
const enterToClasses = useSplitClasses(enterTo)
let enterClasses = useSplitClasses(enter)
let enterFromClasses = useSplitClasses(enterFrom)
let enterToClasses = useSplitClasses(enterTo)
const leaveClasses = useSplitClasses(leave)
const leaveFromClasses = useSplitClasses(leaveFrom)
const leaveToClasses = useSplitClasses(leaveTo)
let leaveClasses = useSplitClasses(leave)
let leaveFromClasses = useSplitClasses(leaveFrom)
let leaveToClasses = useSplitClasses(leaveTo)
const events = useEvents({ beforeEnter, afterEnter, beforeLeave, afterLeave })
let events = useEvents({ beforeEnter, afterEnter, beforeLeave, afterLeave })
React.useEffect(() => {
useEffect(() => {
if (state === TreeStates.Visible && container.current === null) {
throw new Error('Did you forget to passthrough the `ref` to the actual DOM node?')
}
}, [container, state])
// Skipping initial transition
const skip = initial && !appear
let skip = initial && !appear
useIsoMorphicEffect(() => {
const node = container.current
let node = container.current
if (!node) return
if (skip) return
@@ -297,9 +311,9 @@ function TransitionChild<TTag extends React.ElementType = typeof DEFAULT_TRANSIT
leaveToClasses,
])
const propsBag = {}
const propsWeControl = { ref: container }
const passthroughProps = rest
let propsBag = {}
let propsWeControl = { ref: container }
let passthroughProps = rest
return (
<NestingContext.Provider value={nesting}>
@@ -314,28 +328,28 @@ function TransitionChild<TTag extends React.ElementType = typeof DEFAULT_TRANSIT
)
}
export function Transition<TTag extends React.ElementType = typeof DEFAULT_TRANSITION_CHILD_TAG>(
export function Transition<TTag extends ElementType = typeof DEFAULT_TRANSITION_CHILD_TAG>(
props: TransitionChildProps<TTag> & { show: boolean; appear?: boolean }
) {
const { show, appear = false, unmount, ...passthroughProps } = props
let { show, appear = false, unmount, ...passthroughProps } = props as Expand<typeof props>
if (![true, false].includes(show)) {
throw new Error('A <Transition /> is used but it is missing a `show={true | false}` prop.')
}
const [state, setState] = React.useState(show ? TreeStates.Visible : TreeStates.Hidden)
let [state, setState] = useState(show ? TreeStates.Visible : TreeStates.Hidden)
const nestingBag = useNesting(() => {
let nestingBag = useNesting(() => {
setState(TreeStates.Hidden)
})
const initial = useIsInitialRender()
const transitionBag = React.useMemo<TransitionContextValues>(
let initial = useIsInitialRender()
let transitionBag = useMemo<TransitionContextValues>(
() => ({ show, appear: appear || !initial }),
[show, appear, initial]
)
React.useEffect(() => {
useEffect(() => {
if (show) {
setState(TreeStates.Visible)
} else if (!hasChildren(nestingBag)) {
@@ -343,8 +357,8 @@ export function Transition<TTag extends React.ElementType = typeof DEFAULT_TRANS
}
}, [show, nestingBag])
const sharedProps = { unmount }
const propsBag = {}
let sharedProps = { unmount }
let propsBag = {}
return (
<NestingContext.Provider value={nestingBag}>
@@ -352,11 +366,11 @@ export function Transition<TTag extends React.ElementType = typeof DEFAULT_TRANS
{render(
{
...sharedProps,
as: React.Fragment,
as: Fragment,
children: <TransitionChild {...sharedProps} {...passthroughProps} />,
},
propsBag,
React.Fragment,
Fragment,
TransitionChildRenderFeatures,
state === TreeStates.Visible
)}
@@ -8,10 +8,10 @@ beforeEach(() => {
})
it('should be possible to transition', async () => {
const d = disposables()
let d = disposables()
const snapshots: { content: string; recordedAt: bigint }[] = []
const element = document.createElement('div')
let snapshots: { content: string; recordedAt: bigint }[] = []
let element = document.createElement('div')
document.body.appendChild(element)
d.add(
@@ -44,17 +44,17 @@ it('should be possible to transition', async () => {
// Cleanup phase
expect(snapshots[2].content).toEqual('<div class=""></div>')
await d.dispose()
d.dispose()
})
it('should wait the correct amount of time to finish a transition', async () => {
const d = disposables()
let d = disposables()
const snapshots: { content: string; recordedAt: bigint }[] = []
const element = document.createElement('div')
let snapshots: { content: string; recordedAt: bigint }[] = []
let element = document.createElement('div')
document.body.appendChild(element)
const duration = 20
let duration = 20
element.style.transitionDuration = `${duration}ms`
@@ -70,7 +70,7 @@ it('should wait the correct amount of time to finish a transition', async () =>
)
)
const reason = await new Promise(resolve => {
let reason = await new Promise(resolve => {
transition(element, ['enter'], ['enterFrom'], ['enterTo'], resolve)
})
@@ -89,7 +89,7 @@ it('should wait the correct amount of time to finish a transition', async () =>
`<div style="transition-duration: ${duration}ms;" class="enter enterTo"></div>`
)
const estimatedDuration = Number(
let estimatedDuration = Number(
(snapshots[snapshots.length - 1].recordedAt - snapshots[snapshots.length - 2].recordedAt) /
BigInt(1e6)
)
@@ -103,14 +103,14 @@ it('should wait the correct amount of time to finish a transition', async () =>
})
it('should keep the delay time into account', async () => {
const d = disposables()
let d = disposables()
const snapshots: { content: string; recordedAt: bigint }[] = []
const element = document.createElement('div')
let snapshots: { content: string; recordedAt: bigint }[] = []
let element = document.createElement('div')
document.body.appendChild(element)
const duration = 20
const delayDuration = 100
let duration = 20
let delayDuration = 100
element.style.transitionDuration = `${duration}ms`
element.style.transitionDelay = `${delayDuration}ms`
@@ -127,14 +127,14 @@ it('should keep the delay time into account', async () => {
)
)
const reason = await new Promise(resolve => {
let reason = await new Promise(resolve => {
transition(element, ['enter'], ['enterFrom'], ['enterTo'], resolve)
})
await new Promise(resolve => d.nextFrame(resolve))
expect(reason).toBe(Reason.Finished)
const estimatedDuration = Number(
let estimatedDuration = Number(
(snapshots[snapshots.length - 1].recordedAt - snapshots[snapshots.length - 2].recordedAt) /
BigInt(1e6)
)
@@ -143,18 +143,18 @@ it('should keep the delay time into account', async () => {
})
it('should be possible to cancel a transition at any time', async () => {
const d = disposables()
let d = disposables()
const snapshots: {
let snapshots: {
content: string
recordedAt: bigint
relativeTime: number
}[] = []
const element = document.createElement('div')
let element = document.createElement('div')
document.body.appendChild(element)
// This duration is so overkill, however it will demonstrate that we can cancel transitions.
const duration = 5000
let duration = 5000
element.style.transitionDuration = `${duration}ms`
@@ -162,8 +162,8 @@ it('should be possible to cancel a transition at any time', async () => {
reportChanges(
() => document.body.innerHTML,
content => {
const recordedAt = process.hrtime.bigint()
const total = snapshots.length
let recordedAt = process.hrtime.bigint()
let total = snapshots.length
snapshots.push({
content,
@@ -178,7 +178,7 @@ it('should be possible to cancel a transition at any time', async () => {
expect.assertions(2)
// Setup the transition
const cancel = transition(element, ['enter'], ['enterFrom'], ['enterTo'], reason => {
let cancel = transition(element, ['enter'], ['enterFrom'], ['enterTo'], reason => {
expect(reason).toBe(Reason.Cancelled)
})
@@ -15,15 +15,15 @@ export enum Reason {
}
function waitForTransition(node: HTMLElement, done: (reason: Reason) => void) {
const d = disposables()
let d = disposables()
if (!node) return d.dispose
// Safari returns a comma separated list of values, so let's sort them and take the highest value.
const { transitionDuration, transitionDelay } = getComputedStyle(node)
let { transitionDuration, transitionDelay } = getComputedStyle(node)
const [durationMs, delaysMs] = [transitionDuration, transitionDelay].map(value => {
const [resolvedValue = 0] = value
let [durationMs, delaysMs] = [transitionDuration, transitionDelay].map(value => {
let [resolvedValue = 0] = value
.split(',')
// Remove falseys we can't work with
.filter(Boolean)
@@ -62,8 +62,8 @@ export function transition(
to: string[],
done?: (reason: Reason) => void
) {
const d = disposables()
const _done = done !== undefined ? once(done) : () => {}
let d = disposables()
let _done = done !== undefined ? once(done) : () => {}
addClasses(node, ...base, ...from)
@@ -1,4 +1,4 @@
import * as React from 'react'
import { useState } from 'react'
beforeEach(() => {
id = 0
@@ -10,6 +10,6 @@ function generateId() {
}
export function useId() {
const [id] = React.useState(generateId)
const [id] = useState(generateId)
return id
}
@@ -1,9 +1,9 @@
import * as React from 'react'
import { useState, useRef } from 'react'
import { useIsoMorphicEffect } from './use-iso-morphic-effect'
export function useComputed<T>(cb: () => T, dependencies: React.DependencyList) {
const [value, setValue] = React.useState(cb)
const cbRef = React.useRef(cb)
let [value, setValue] = useState(cb)
let cbRef = useRef(cb)
useIsoMorphicEffect(() => {
cbRef.current = cb
}, [cb])
@@ -1,10 +1,10 @@
import * as React from 'react'
import { useState, useEffect } from 'react'
import { disposables } from '../utils/disposables'
export function useDisposables() {
// Using useState instead of useRef so that we can use the initializer function.
const [d] = React.useState(disposables)
React.useEffect(() => () => d.dispose(), [d])
let [d] = useState(disposables)
useEffect(() => () => d.dispose(), [d])
return d
}
@@ -1,4 +1,4 @@
import * as React from 'react'
import { useState, useEffect } from 'react'
import { useIsoMorphicEffect } from './use-iso-morphic-effect'
// We used a "simple" approach first which worked for SSR and rehydration on the client. However we
@@ -14,13 +14,13 @@ function generateId() {
}
export function useId() {
const [id, setId] = React.useState(state.serverHandoffComplete ? generateId : null)
let [id, setId] = useState(state.serverHandoffComplete ? generateId : null)
useIsoMorphicEffect(() => {
if (id === null) setId(generateId())
}, [id])
React.useEffect(() => {
useEffect(() => {
if (state.serverHandoffComplete === false) state.serverHandoffComplete = true
}, [])
@@ -1,9 +1,9 @@
import * as React from 'react'
import { useRef, useEffect } from 'react'
export function useIsInitialRender() {
const initial = React.useRef(true)
let initial = useRef(true)
React.useEffect(() => {
useEffect(() => {
initial.current = false
}, [])
@@ -1,9 +1,9 @@
import * as React from 'react'
import { useRef, useEffect } from 'react'
export function useIsMounted() {
const mounted = React.useRef(true)
let mounted = useRef(true)
React.useEffect(() => {
useEffect(() => {
return () => {
mounted.current = false
}
@@ -1,4 +1,3 @@
import * as React from 'react'
import { useLayoutEffect, useEffect } from 'react'
export const useIsoMorphicEffect =
typeof window !== 'undefined' ? React.useLayoutEffect : React.useEffect
export const useIsoMorphicEffect = typeof window !== 'undefined' ? useLayoutEffect : useEffect
@@ -1,9 +1,9 @@
import * as React from 'react'
import { useCallback } from 'react'
export function useSyncRefs<TType>(
...refs: (React.MutableRefObject<TType> | ((instance: TType) => void) | null)[]
) {
return React.useCallback(
return useCallback(
(value: TType) => {
refs.forEach(ref => {
if (ref === null) return
+2 -2
View File
@@ -1,9 +1,9 @@
import * as TailwindUI from './index'
import * as HeadlessUI from './index'
/**
* Looks a bit of a silly test, however this ensures that we don't accidentally expose something to
* the outside world that we didn't want!
*/
it('should expose the correct components', () => {
expect(Object.keys(TailwindUI)).toEqual(['Transition', 'Menu', 'Listbox', 'Switch'])
expect(Object.keys(HeadlessUI)).toEqual(['Transition', 'Menu', 'Listbox', 'Switch'])
})
@@ -580,7 +580,7 @@ export function assertLabelValue(element: HTMLElement | null, value: string) {
if (element === null) return expect(element).not.toBe(null)
if (element.hasAttribute('aria-labelledby')) {
const ids = element.getAttribute('aria-labelledby')!.split(' ')
let ids = element.getAttribute('aria-labelledby')!.split(' ')
expect(ids.map(id => document.getElementById(id)?.textContent).join(' ')).toEqual(value)
return
}
@@ -8,11 +8,11 @@ export async function executeTimeline(
element: JSX.Element,
steps: ((tools: ReturnType<typeof render>) => (null | number)[])[]
) {
const d = disposables()
const snapshots: { content: DocumentFragment; recordedAt: bigint }[] = []
let d = disposables()
let snapshots: { content: DocumentFragment; recordedAt: bigint }[] = []
//
const tools = render(element)
let tools = render(element)
// Start listening for changes
d.add(
@@ -30,13 +30,13 @@ export async function executeTimeline(
// We start with a `null` value because we will start with a snapshot even _before_ things start
// happening.
const timestamps: (null | number)[] = [null]
let timestamps: (null | number)[] = [null]
//
await steps.reduce(async (chain, step) => {
await chain
const durations = step(tools)
let durations = step(tools)
// Note: The following calls are just in place to ensure that **we** waited long enough for the
// transitions to take place. This has no impact on the actual transitions. Above where the
@@ -45,7 +45,7 @@ export async function executeTimeline(
timestamps.push(...durations)
const totalDuration = durations
let totalDuration = durations
.filter((duration): duration is number => duration !== null)
.reduce((total, current) => total + current, 0)
@@ -63,7 +63,7 @@ export async function executeTimeline(
throw new Error('We could not record any changes')
}
const uniqueSnapshots = snapshots
let uniqueSnapshots = snapshots
// Only keep the snapshots that are unique. Multiple snapshots of the same
// content are a bit useless for us.
.filter((snapshot, i) => {
@@ -79,7 +79,7 @@ export async function executeTimeline(
i === 0 ? 0 : Number((snapshot.recordedAt - all[i - 1].recordedAt) / BigInt(1e6)),
}))
const diffed = uniqueSnapshots
let diffed = uniqueSnapshots
.map((call, i) => {
// Skip initial render, because there is nothing to compare with
if (i === 0) return false
@@ -112,7 +112,7 @@ export async function executeTimeline(
.filter(Boolean)
.join('\n\n')
await d.dispose()
d.dispose()
return diffed
}
@@ -131,13 +131,13 @@ executeTimeline.fullTransition = (duration: number) => {
}
// Assuming that we run at 60 frames per second
const frame = 1000 / 60
let frame = 1000 / 60
function isWithinFrame(actual: number, expected: number, frames = 2) {
const buffer = frame * frames
let buffer = frame * frames
const min = expected - buffer
const max = expected + buffer
let min = expected - buffer
let max = expected + buffer
return actual >= min && actual <= max
}
@@ -8,7 +8,7 @@ function nextFrame(cb: Function): void {
)
}
export const Keys: Record<string, Partial<KeyboardEvent>> = {
export let Keys: Record<string, Partial<KeyboardEvent>> = {
Space: { key: ' ', keyCode: 32, charCode: 32 },
Enter: { key: 'Enter', keyCode: 13, charCode: 13 },
Escape: { key: 'Escape', keyCode: 27, charCode: 27 },
@@ -276,13 +276,13 @@ export async function mouseLeave(element: Document | Element | Window | null) {
// ---
function focusNext(event: Partial<KeyboardEvent>) {
const direction = event.shiftKey ? -1 : +1
const focusableElements = getFocusableElements()
const total = focusableElements.length
let direction = event.shiftKey ? -1 : +1
let focusableElements = getFocusableElements()
let total = focusableElements.length
function innerFocusNext(offset = 0): Element {
const currentIdx = focusableElements.indexOf(document.activeElement as HTMLElement)
const next = focusableElements[(currentIdx + total + direction + offset) % total] as HTMLElement
let currentIdx = focusableElements.indexOf(document.activeElement as HTMLElement)
let next = focusableElements[(currentIdx + total + direction + offset) % total] as HTMLElement
if (next) next?.focus({ preventScroll: true })
@@ -295,7 +295,7 @@ function focusNext(event: Partial<KeyboardEvent>) {
// Credit:
// - https://stackoverflow.com/a/30753870
const focusableSelector = [
let focusableSelector = [
'[contentEditable=true]',
'[tabindex]',
'a[href]',
@@ -1,12 +1,12 @@
import { disposables } from '../utils/disposables'
export function reportChanges<TType>(key: () => TType, onChange: (value: TType) => void) {
const d = disposables()
let d = disposables()
let previous: TType
function track() {
const next = key()
let next = key()
if (previous !== next) {
previous = next
onChange(next)
@@ -8,7 +8,7 @@ export function suppressConsoleLogs<T extends unknown[]>(
type: FunctionPropertyNames<typeof global.console> = 'error'
) {
return (...args: T) => {
const spy = jest.spyOn(global.console, type).mockImplementation(jest.fn())
let spy = jest.spyOn(global.console, type).mockImplementation(jest.fn())
return new Promise<unknown>((resolve, reject) => {
Promise.resolve(cb(...args)).then(resolve, reject)
+2
View File
@@ -3,6 +3,8 @@
const __: unique symbol = Symbol('__placeholder__')
export type __ = typeof __
export type Expand<T> = T extends infer O ? { [K in keyof O]: O[K] } : never
export type PropsOf<TTag = any> = TTag extends React.ElementType
? React.ComponentProps<TTag>
: never
@@ -31,19 +31,19 @@ export function calculateActiveIndex<TItem>(
resolveDisabled(item: TItem): boolean
}
) {
const items = resolvers.resolveItems()
let items = resolvers.resolveItems()
if (items.length <= 0) return null
const currentActiveIndex = resolvers.resolveActiveIndex()
const activeIndex = currentActiveIndex ?? -1
let currentActiveIndex = resolvers.resolveActiveIndex()
let activeIndex = currentActiveIndex ?? -1
const nextActiveIndex = (() => {
let nextActiveIndex = (() => {
switch (action.focus) {
case Focus.First:
return items.findIndex(item => !resolvers.resolveDisabled(item))
case Focus.Previous: {
const idx = items
let idx = items
.slice()
.reverse()
.findIndex((item, idx, all) => {
@@ -61,7 +61,7 @@ export function calculateActiveIndex<TItem>(
})
case Focus.Last: {
const idx = items
let idx = items
.slice()
.reverse()
.findIndex(item => !resolvers.resolveDisabled(item))
@@ -1,9 +1,9 @@
export function disposables() {
const disposables: Function[] = []
let disposables: Function[] = []
const api = {
let api = {
requestAnimationFrame(...args: Parameters<typeof requestAnimationFrame>) {
const raf = requestAnimationFrame(...args)
let raf = requestAnimationFrame(...args)
api.add(() => cancelAnimationFrame(raf))
},
@@ -14,7 +14,7 @@ export function disposables() {
},
setTimeout(...args: Parameters<typeof setTimeout>) {
const timer = setTimeout(...args)
let timer = setTimeout(...args)
api.add(() => clearTimeout(timer))
},
@@ -23,7 +23,9 @@ export function disposables() {
},
dispose() {
disposables.splice(0).forEach(dispose => dispose())
for (let dispose of disposables.splice(0)) {
dispose()
}
},
}
@@ -4,11 +4,11 @@ export function match<TValue extends string | number = string, TReturnValue = un
...args: any[]
): TReturnValue {
if (value in lookup) {
const returnValue = lookup[value]
let returnValue = lookup[value]
return typeof returnValue === 'function' ? returnValue(...args) : returnValue
}
const error = new Error(
let error = new Error(
`Tried to handle "${value}" but there is no handler defined. Only defined handlers are: ${Object.keys(
lookup
)
+2 -4
View File
@@ -1,10 +1,8 @@
export function once<T>(cb: (...args: T[]) => void) {
const state = { called: false }
let state = { called: false }
return (...args: T[]) => {
if (state.called) {
return
}
if (state.called) return
state.called = true
return cb(...args)
}
@@ -1,4 +1,4 @@
import React from 'react'
import React, { ElementType, createRef, Ref, Fragment } from 'react'
import { render as testRender, prettyDOM, getByTestId } from '@testing-library/react'
import { suppressConsoleLogs } from '../test-utils/suppress-console-logs'
@@ -12,8 +12,8 @@ function contents() {
}
describe('Default functionality', () => {
const bag = {}
function Dummy<TTag extends React.ElementType = 'div'>(
let bag = {}
function Dummy<TTag extends ElementType = 'div'>(
props: Props<TTag> & Partial<{ a: any; b: any; c: any }>
) {
return <div data-testid="wrapper">{render(props, bag, 'div')}</div>
@@ -58,16 +58,16 @@ describe('Default functionality', () => {
})
it('should be possible to add a ref with a different name', () => {
const ref = React.createRef()
let ref = createRef()
function MyComponent<T extends React.ElementType = 'div'>({
function MyComponent<T extends ElementType = 'div'>({
innerRef,
...props
}: Props<T> & { innerRef: React.Ref<HTMLDivElement> }) {
}: Props<T> & { innerRef: Ref<HTMLDivElement> }) {
return <div ref={innerRef} {...props} />
}
function OtherDummy<TTag extends React.ElementType = 'div'>(props: Props<TTag>) {
function OtherDummy<TTag extends ElementType = 'div'>(props: Props<TTag>) {
return <div data-testid="wrapper">{render({ ...props, ref }, bag, 'div')}</div>
}
@@ -134,8 +134,8 @@ describe('Default functionality', () => {
`)
})
it('should be possible to render the children only when the `as` prop is set to React.Fragment', () => {
testRender(<Dummy as={React.Fragment}>Contents</Dummy>)
it('should be possible to render the children only when the `as` prop is set to Fragment', () => {
testRender(<Dummy as={Fragment}>Contents</Dummy>)
expect(contents()).toMatchInlineSnapshot(`
"<div
@@ -146,9 +146,9 @@ describe('Default functionality', () => {
`)
})
it('should forward all the props to the first child when using an as={React.Fragment}', () => {
it('should forward all the props to the first child when using an as={Fragment}', () => {
testRender(
<Dummy as={React.Fragment} a={1} b={1}>
<Dummy as={Fragment} a={1} b={1}>
{() => <span>Contents</span>}
</Dummy>
)
@@ -168,14 +168,14 @@ describe('Default functionality', () => {
})
it(
'should error when we are rendering a React.Fragment with multiple children',
'should error when we are rendering a Fragment with multiple children',
suppressConsoleLogs(() => {
expect.assertions(1)
return expect(() => {
testRender(
// @ts-expect-error className cannot be applied to a React.Fragment
<Dummy as={React.Fragment} className="p-12">
// @ts-expect-error className cannot be applied to a Fragment
<Dummy as={Fragment} className="p-12">
<span>Contents A</span>
<span>Contents B</span>
</Dummy>
@@ -184,9 +184,9 @@ describe('Default functionality', () => {
})
)
it("should not error when we are rendering a React.Fragment with multiple children when we don't passthrough additional props", () => {
it("should not error when we are rendering a Fragment with multiple children when we don't passthrough additional props", () => {
testRender(
<Dummy as={React.Fragment}>
<Dummy as={Fragment}>
<span>Contents A</span>
<span>Contents B</span>
</Dummy>
@@ -207,14 +207,14 @@ describe('Default functionality', () => {
})
it(
'should error when we are applying props to a React.Fragment when we do not have a dedicated element',
'should error when we are applying props to a Fragment when we do not have a dedicated element',
suppressConsoleLogs(() => {
expect.assertions(1)
return expect(() => {
testRender(
// @ts-expect-error className cannot be applied to a React.Fragment
<Dummy as={React.Fragment} className="p-12">
// @ts-expect-error className cannot be applied to a Fragment
<Dummy as={Fragment} className="p-12">
Contents
</Dummy>
)
@@ -270,12 +270,12 @@ function testStaticFeature(Dummy) {
// component in a Transition for example so that the Transition component can control the
// showing/hiding based on the `show` prop AND the state of the transition.
describe('Features.Static', () => {
const bag = {}
const EnabledFeatures = Features.Static
function Dummy<TTag extends React.ElementType = 'div'>(
let bag = {}
let EnabledFeatures = Features.Static
function Dummy<TTag extends ElementType = 'div'>(
props: Props<TTag> & { show: boolean } & PropsForFeatures<typeof EnabledFeatures>
) {
const { show, ...rest } = props
let { show, ...rest } = props
return <div data-testid="wrapper">{render(rest, bag, 'div', EnabledFeatures, show)}</div>
}
@@ -364,12 +364,12 @@ function testRenderStrategyFeature(Dummy) {
}
describe('Features.RenderStrategy', () => {
const bag = {}
const EnabledFeatures = Features.RenderStrategy
function Dummy<TTag extends React.ElementType = 'div'>(
let bag = {}
let EnabledFeatures = Features.RenderStrategy
function Dummy<TTag extends ElementType = 'div'>(
props: Props<TTag> & { show: boolean } & PropsForFeatures<typeof EnabledFeatures>
) {
const { show, ...rest } = props
let { show, ...rest } = props
return <div data-testid="wrapper">{render(rest, bag, 'div', EnabledFeatures, show)}</div>
}
@@ -380,12 +380,12 @@ describe('Features.RenderStrategy', () => {
// This should enable the `static` and `unmount` features. However they can't be used together!
describe('Features.Static | Features.RenderStrategy', () => {
const bag = {}
const EnabledFeatures = Features.Static | Features.RenderStrategy
function Dummy<TTag extends React.ElementType = 'div'>(
let bag = {}
let EnabledFeatures = Features.Static | Features.RenderStrategy
function Dummy<TTag extends ElementType = 'div'>(
props: Props<TTag> & { show: boolean } & PropsForFeatures<typeof EnabledFeatures>
) {
const { show, ...rest } = props
let { show, ...rest } = props
return <div data-testid="wrapper">{render(rest, bag, 'div', EnabledFeatures, show)}</div>
}
+35 -29
View File
@@ -1,5 +1,15 @@
import * as React from 'react'
import { Props, XOR, __ } from '../types'
import {
Fragment,
cloneElement,
createElement,
forwardRef,
isValidElement,
// Types
ElementType,
ReactElement,
} from 'react'
import { Props, XOR, __, Expand } from '../types'
import { match } from './match'
export enum Features {
@@ -36,28 +46,28 @@ export type PropsForFeatures<T extends Features> = XOR<
PropsForFeature<T, Features.RenderStrategy, { unmount?: boolean }>
>
export function render<TFeature extends Features, TTag extends React.ElementType, TBag>(
props: Props<TTag, TBag, any> & PropsForFeatures<TFeature>,
export function render<TFeature extends Features, TTag extends ElementType, TBag>(
props: Expand<Props<TTag, TBag, any> & PropsForFeatures<TFeature>>,
propsBag: TBag,
defaultTag: React.ElementType,
defaultTag: ElementType,
features?: TFeature,
visible: boolean = true
) {
// Visible always render
if (visible) return _render(props, propsBag, defaultTag)
const featureFlags = features ?? Features.None
let featureFlags = features ?? Features.None
if (featureFlags & Features.Static) {
const { static: isStatic = false, ...rest } = props as PropsForFeatures<Features.Static>
let { static: isStatic = false, ...rest } = props as PropsForFeatures<Features.Static>
// When the `static` prop is passed as `true`, then the user is in control, thus we don't care about anything else
if (isStatic) return _render(rest, propsBag, defaultTag)
}
if (featureFlags & Features.RenderStrategy) {
const { unmount = true, ...rest } = props as PropsForFeatures<Features.RenderStrategy>
const strategy = unmount ? RenderStrategy.Unmount : RenderStrategy.Hidden
let { unmount = true, ...rest } = props as PropsForFeatures<Features.RenderStrategy>
let strategy = unmount ? RenderStrategy.Unmount : RenderStrategy.Hidden
return match(strategy, {
[RenderStrategy.Unmount]() {
@@ -77,40 +87,40 @@ export function render<TFeature extends Features, TTag extends React.ElementType
return _render(props, propsBag, defaultTag)
}
function _render<TTag extends React.ElementType, TBag>(
props: Props<TTag, TBag> & { ref?: unknown },
function _render<TTag extends ElementType, TBag>(
props: Expand<Props<TTag, TBag> & { ref?: unknown }>,
bag: TBag,
tag: React.ElementType
tag: ElementType
) {
const { as: Component = tag, children, refName = 'ref', ...passThroughProps } = omit(props, [
let { as: Component = tag, children, refName = 'ref', ...passThroughProps } = omit(props, [
'unmount',
'static',
])
// This allows us to use `<HeadlessUIComponent as={MyComopnent} refName="innerRef" />`
const refRelatedProps = props.ref !== undefined ? { [refName]: props.ref } : {}
let refRelatedProps = props.ref !== undefined ? { [refName]: props.ref } : {}
const resolvedChildren = (typeof children === 'function' ? children(bag) : children) as
| React.ReactElement
| React.ReactElement[]
let resolvedChildren = (typeof children === 'function' ? children(bag) : children) as
| ReactElement
| ReactElement[]
if (Component === React.Fragment) {
if (Component === Fragment) {
if (Object.keys(passThroughProps).length > 0) {
if (Array.isArray(resolvedChildren) && resolvedChildren.length > 1) {
const err = new Error('You should only render 1 child')
let err = new Error('You should only render 1 child')
if (Error.captureStackTrace) Error.captureStackTrace(err, _render)
throw err
}
if (!React.isValidElement(resolvedChildren)) {
const err = new Error(
if (!isValidElement(resolvedChildren)) {
let err = new Error(
`You should render an element as a child. Did you forget the as="..." prop?`
)
if (Error.captureStackTrace) Error.captureStackTrace(err, _render)
throw err
}
return React.cloneElement(
return cloneElement(
resolvedChildren,
Object.assign(
{},
@@ -124,13 +134,9 @@ function _render<TTag extends React.ElementType, TBag>(
}
}
return React.createElement(
return createElement(
Component,
Object.assign(
{},
omit(passThroughProps, ['ref']),
Component !== React.Fragment && refRelatedProps
),
Object.assign({}, omit(passThroughProps, ['ref']), Component !== Fragment && refRelatedProps),
resolvedChildren
)
}
@@ -177,7 +183,7 @@ function mergeEventFunctions(
* wrap it in a forwardRef so that we _can_ passthrough the ref
*/
export function forwardRefWithAs<T>(component: T): T {
return React.forwardRef((component as unknown) as any) as any
return forwardRef((component as unknown) as any) as any
}
function compact<T extends Record<any, any>>(object: T) {
@@ -87,8 +87,8 @@ export default {
KeyCaster,
},
setup() {
const route = useRoute()
const sourceCode = computed(() => source[route.path])
let route = useRoute()
let sourceCode = computed(() => source[route.path])
return { sourceCode }
},
@@ -15,9 +15,9 @@
<script>
import { defineComponent, ref } from 'vue'
const isMac = navigator.userAgent.indexOf('Mac OS X') !== -1
let isMac = navigator.userAgent.indexOf('Mac OS X') !== -1
const KeyDisplay = isMac
let KeyDisplay = isMac
? {
ArrowUp: '↑',
ArrowDown: '↓',
@@ -57,7 +57,7 @@ const KeyDisplay = isMac
export default defineComponent({
setup() {
const keys = ref([])
let keys = ref([])
window.addEventListener('keydown', event => {
keys.value.unshift(
@@ -12,7 +12,7 @@ import { defineComponent, h } from 'vue'
import { RouterLink } from 'vue-router'
import routes from '../routes.json'
const Examples = defineComponent({
let Examples = defineComponent({
props: ['examples'],
setup(props) {
return () => {
@@ -93,7 +93,7 @@ function classNames(...classes) {
export default {
components: { Listbox, ListboxLabel, ListboxButton, ListboxOptions, ListboxOption },
setup(props, context) {
const people = [
let people = [
{ id: 1, name: 'Wade Cooper' },
{ id: 2, name: 'Arlene Mccoy' },
{ id: 3, name: 'Devon Webb' },
@@ -106,7 +106,7 @@ export default {
{ id: 10, name: 'Emil Schaefer' },
]
const active = ref(people[Math.floor(Math.random() * people.length)])
let active = ref(people[Math.floor(Math.random() * people.length)])
return {
people,
@@ -179,7 +179,7 @@ function classNames(...classes) {
export default {
components: { Listbox, ListboxLabel, ListboxButton, ListboxOptions, ListboxOption },
setup(props, context) {
const people = [
let people = [
{ id: 1, name: 'Wade Cooper' },
{ id: 2, name: 'Arlene Mccoy' },
{ id: 3, name: 'Devon Webb' },
@@ -192,7 +192,7 @@ export default {
{ id: 10, name: 'Emil Schaefer' },
]
const active = ref(people[Math.floor(Math.random() * people.length)])
let active = ref(people[Math.floor(Math.random() * people.length)])
return {
people,
@@ -62,7 +62,7 @@ function classNames(...classes) {
export default {
components: { Menu, MenuButton, MenuItems, MenuItem },
setup(props, context) {
const [trigger, container] = usePopper({
let [trigger, container] = usePopper({
placement: 'bottom-end',
strategy: 'fixed',
modifiers: [{ name: 'offset', options: { offset: [0, 10] } }],
@@ -70,7 +70,7 @@ function classNames(...classes) {
export default {
components: { Menu, MenuButton, MenuItems, MenuItem },
setup(props, context) {
const [trigger, container] = usePopper({
let [trigger, container] = usePopper({
placement: 'bottom-end',
strategy: 'fixed',
modifiers: [{ name: 'offset', options: { offset: [0, 10] } }],
@@ -48,7 +48,7 @@ function classNames(...classes) {
return classes.filter(Boolean).join(' ')
}
const CustomMenuItem = defineComponent({
let CustomMenuItem = defineComponent({
components: { Menu, MenuButton, MenuItems, MenuItem },
setup(props, { slots }) {
return () => {
@@ -24,7 +24,7 @@ function classNames(...classes) {
export default {
components: { SwitchGroup, Switch, SwitchLabel },
setup(props, context) {
const state = ref(false)
let state = ref(false)
return {
state,
@@ -2,18 +2,18 @@ import { ref, onMounted, watchEffect } from 'vue'
import { createPopper } from '@popperjs/core'
export function usePopper(options) {
const reference = ref(null)
const popper = ref(null)
let reference = ref(null)
let popper = ref(null)
onMounted(() => {
watchEffect(onInvalidate => {
const popperEl = popper.value.el || popper.value
const referenceEl = reference.value.el || reference.value
let popperEl = popper.value.el || popper.value
let referenceEl = reference.value.el || reference.value
if (!(referenceEl instanceof HTMLElement)) return
if (!(popperEl instanceof HTMLElement)) return
const { destroy } = createPopper(referenceEl, popperEl, options)
let { destroy } = createPopper(referenceEl, popperEl, options)
onInvalidate(destroy)
})
@@ -4,7 +4,7 @@ import routes from './routes.json'
function buildRoutes(routes) {
return routes.map(route => {
const definition = {
let definition = {
path: route.path,
component: route.component ? lookup[route.component] : RouterView,
}
@@ -45,7 +45,7 @@ beforeAll(() => {
afterAll(() => jest.restoreAllMocks())
function renderTemplate(input: string | Partial<Parameters<typeof defineComponent>[0]>) {
const defaultComponents = { Listbox, ListboxLabel, ListboxButton, ListboxOptions, ListboxOption }
let defaultComponents = { Listbox, ListboxLabel, ListboxButton, ListboxOptions, ListboxOption }
if (typeof input === 'string') {
return render(defineComponent({ template: input, components: defaultComponents }))
@@ -472,7 +472,7 @@ describe('Rendering composition', () => {
// Open Listbox
await click(getListboxButton())
const options = getListboxOptions()
let options = getListboxOptions()
// Verify correct classNames
expect('' + options[0].classList).toEqual(
@@ -597,7 +597,7 @@ describe('Keyboard interactions', () => {
assertListboxButtonLinkedWithListbox()
// Verify we have listbox options
const options = getListboxOptions()
let options = getListboxOptions()
expect(options).toHaveLength(3)
options.forEach(option => assertListboxOption(option, { selected: false }))
@@ -684,7 +684,7 @@ describe('Keyboard interactions', () => {
assertListboxButtonLinkedWithListbox()
// Verify we have listbox options
const options = getListboxOptions()
let options = getListboxOptions()
expect(options).toHaveLength(3)
options.forEach((option, i) => assertListboxOption(option, { selected: i === 1 }))
@@ -733,7 +733,7 @@ describe('Keyboard interactions', () => {
assertActiveElement(getListbox())
assertListboxButtonLinkedWithListbox()
const options = getListboxOptions()
let options = getListboxOptions()
// Hover over Option A
await mouseMove(options[0])
@@ -772,12 +772,12 @@ describe('Keyboard interactions', () => {
</Listbox>
`,
setup: () => {
const options = [
let options = [
{ id: 'a', name: 'Option A' },
{ id: 'b', name: 'Option B' },
{ id: 'c', name: 'Option C' },
]
const value = ref(options[1])
let value = ref(options[1])
return { value, options }
},
@@ -805,7 +805,7 @@ describe('Keyboard interactions', () => {
assertListboxButtonLinkedWithListbox()
// Verify we have listbox options
const options = getListboxOptions()
let options = getListboxOptions()
expect(options).toHaveLength(3)
options.forEach((option, i) => assertListboxOption(option, { selected: i === 1 }))
@@ -872,7 +872,7 @@ describe('Keyboard interactions', () => {
// Open listbox
await press(Keys.Enter)
const options = getListboxOptions()
let options = getListboxOptions()
// Verify that the first non-disabled listbox option is active
assertActiveListboxOption(options[1])
@@ -912,7 +912,7 @@ describe('Keyboard interactions', () => {
// Open listbox
await press(Keys.Enter)
const options = getListboxOptions()
let options = getListboxOptions()
// Verify that the first non-disabled listbox option is active
assertActiveListboxOption(options[2])
@@ -1002,7 +1002,7 @@ describe('Keyboard interactions', () => {
it(
'should be possible to close the listbox with Enter and choose the active listbox option',
suppressConsoleLogs(async () => {
const handleChange = jest.fn()
let handleChange = jest.fn()
renderTemplate({
template: `
<Listbox v-model="value">
@@ -1015,7 +1015,7 @@ describe('Keyboard interactions', () => {
</Listbox>
`,
setup() {
const value = ref(null)
let value = ref(null)
watch([value], () => handleChange(value.value))
return { value }
},
@@ -1034,7 +1034,7 @@ describe('Keyboard interactions', () => {
assertListboxButton({ state: ListboxState.Visible })
// Activate the first listbox option
const options = getListboxOptions()
let options = getListboxOptions()
await mouseMove(options[0])
// Choose option, and close listbox
@@ -1100,7 +1100,7 @@ describe('Keyboard interactions', () => {
assertListboxButtonLinkedWithListbox()
// Verify we have listbox options
const options = getListboxOptions()
let options = getListboxOptions()
expect(options).toHaveLength(3)
options.forEach(option => assertListboxOption(option))
assertActiveListboxOption(options[0])
@@ -1184,7 +1184,7 @@ describe('Keyboard interactions', () => {
assertListboxButtonLinkedWithListbox()
// Verify we have listbox options
const options = getListboxOptions()
let options = getListboxOptions()
expect(options).toHaveLength(3)
options.forEach((option, i) => assertListboxOption(option, { selected: i === 1 }))
@@ -1251,7 +1251,7 @@ describe('Keyboard interactions', () => {
// Open listbox
await press(Keys.Space)
const options = getListboxOptions()
let options = getListboxOptions()
// Verify that the first non-disabled listbox option is active
assertActiveListboxOption(options[1])
@@ -1291,7 +1291,7 @@ describe('Keyboard interactions', () => {
// Open listbox
await press(Keys.Space)
const options = getListboxOptions()
let options = getListboxOptions()
// Verify that the first non-disabled listbox option is active
assertActiveListboxOption(options[2])
@@ -1340,7 +1340,7 @@ describe('Keyboard interactions', () => {
it(
'should be possible to close the listbox with Space and choose the active listbox option',
suppressConsoleLogs(async () => {
const handleChange = jest.fn()
let handleChange = jest.fn()
renderTemplate({
template: `
<Listbox v-model="value">
@@ -1353,7 +1353,7 @@ describe('Keyboard interactions', () => {
</Listbox>
`,
setup() {
const value = ref(null)
let value = ref(null)
watch([value], () => handleChange(value.value))
return { value }
},
@@ -1372,7 +1372,7 @@ describe('Keyboard interactions', () => {
assertListboxButton({ state: ListboxState.Visible })
// Activate the first listbox option
const options = getListboxOptions()
let options = getListboxOptions()
await mouseMove(options[0])
// Choose option, and close listbox
@@ -1484,7 +1484,7 @@ describe('Keyboard interactions', () => {
assertListboxButtonLinkedWithListbox()
// Verify we have listbox options
const options = getListboxOptions()
let options = getListboxOptions()
expect(options).toHaveLength(3)
options.forEach(option => assertListboxOption(option))
assertActiveListboxOption(options[0])
@@ -1538,7 +1538,7 @@ describe('Keyboard interactions', () => {
assertListboxButtonLinkedWithListbox()
// Verify we have listbox options
const options = getListboxOptions()
let options = getListboxOptions()
expect(options).toHaveLength(3)
options.forEach(option => assertListboxOption(option))
assertActiveListboxOption(options[0])
@@ -1594,7 +1594,7 @@ describe('Keyboard interactions', () => {
assertListboxButtonLinkedWithListbox()
// Verify we have listbox options
const options = getListboxOptions()
let options = getListboxOptions()
expect(options).toHaveLength(3)
options.forEach(option => assertListboxOption(option))
@@ -1680,7 +1680,7 @@ describe('Keyboard interactions', () => {
assertListboxButtonLinkedWithListbox()
// Verify we have listbox options
const options = getListboxOptions()
let options = getListboxOptions()
expect(options).toHaveLength(3)
options.forEach((option, i) => assertListboxOption(option, { selected: i === 1 }))
@@ -1746,7 +1746,7 @@ describe('Keyboard interactions', () => {
await press(Keys.Enter)
// Verify we have listbox options
const options = getListboxOptions()
let options = getListboxOptions()
expect(options).toHaveLength(3)
options.forEach(option => assertListboxOption(option))
assertActiveListboxOption(options[0])
@@ -1797,7 +1797,7 @@ describe('Keyboard interactions', () => {
await press(Keys.Enter)
// Verify we have listbox options
const options = getListboxOptions()
let options = getListboxOptions()
expect(options).toHaveLength(3)
options.forEach(option => assertListboxOption(option))
assertActiveListboxOption(options[1])
@@ -1842,7 +1842,7 @@ describe('Keyboard interactions', () => {
await press(Keys.Enter)
// Verify we have listbox options
const options = getListboxOptions()
let options = getListboxOptions()
expect(options).toHaveLength(3)
options.forEach(option => assertListboxOption(option))
assertActiveListboxOption(options[2])
@@ -1890,7 +1890,7 @@ describe('Keyboard interactions', () => {
assertListboxButtonLinkedWithListbox()
// Verify we have listbox options
const options = getListboxOptions()
let options = getListboxOptions()
expect(options).toHaveLength(3)
options.forEach(option => assertListboxOption(option))
@@ -1976,7 +1976,7 @@ describe('Keyboard interactions', () => {
assertListboxButtonLinkedWithListbox()
// Verify we have listbox options
const options = getListboxOptions()
let options = getListboxOptions()
expect(options).toHaveLength(3)
options.forEach((option, i) => assertListboxOption(option, { selected: i === 1 }))
@@ -2046,7 +2046,7 @@ describe('Keyboard interactions', () => {
await press(Keys.ArrowUp)
// Verify we have listbox options
const options = getListboxOptions()
let options = getListboxOptions()
expect(options).toHaveLength(3)
options.forEach(option => assertListboxOption(option))
assertActiveListboxOption(options[0])
@@ -2087,7 +2087,7 @@ describe('Keyboard interactions', () => {
await press(Keys.Enter)
// Verify we have listbox options
const options = getListboxOptions()
let options = getListboxOptions()
expect(options).toHaveLength(3)
options.forEach(option => assertListboxOption(option))
assertActiveListboxOption(options[2])
@@ -2141,7 +2141,7 @@ describe('Keyboard interactions', () => {
assertListboxButtonLinkedWithListbox()
// Verify we have listbox options
const options = getListboxOptions()
let options = getListboxOptions()
expect(options).toHaveLength(3)
options.forEach(option => assertListboxOption(option))
assertActiveListboxOption(options[2])
@@ -2185,7 +2185,7 @@ describe('Keyboard interactions', () => {
// Open listbox
await press(Keys.Enter)
const options = getListboxOptions()
let options = getListboxOptions()
// We should be on the first option
assertActiveListboxOption(options[0])
@@ -2224,7 +2224,7 @@ describe('Keyboard interactions', () => {
// Open listbox
await press(Keys.Enter)
const options = getListboxOptions()
let options = getListboxOptions()
// We should be on the first option
assertActiveListboxOption(options[0])
@@ -2268,7 +2268,7 @@ describe('Keyboard interactions', () => {
// We should not be able to go to the end
await press(Keys.End)
const options = getListboxOptions()
let options = getListboxOptions()
assertActiveListboxOption(options[0])
})
)
@@ -2337,7 +2337,7 @@ describe('Keyboard interactions', () => {
// Open listbox
await press(Keys.Enter)
const options = getListboxOptions()
let options = getListboxOptions()
// We should be on the first option
assertActiveListboxOption(options[0])
@@ -2376,7 +2376,7 @@ describe('Keyboard interactions', () => {
// Open listbox
await press(Keys.Enter)
const options = getListboxOptions()
let options = getListboxOptions()
// We should be on the first option
assertActiveListboxOption(options[0])
@@ -2420,7 +2420,7 @@ describe('Keyboard interactions', () => {
// We should not be able to go to the end
await press(Keys.PageDown)
const options = getListboxOptions()
let options = getListboxOptions()
assertActiveListboxOption(options[0])
})
)
@@ -2489,7 +2489,7 @@ describe('Keyboard interactions', () => {
// Open listbox
await press(Keys.ArrowUp)
const options = getListboxOptions()
let options = getListboxOptions()
// We should be on the last option
assertActiveListboxOption(options[2])
@@ -2531,7 +2531,7 @@ describe('Keyboard interactions', () => {
// We should not be able to go to the end
await press(Keys.Home)
const options = getListboxOptions()
let options = getListboxOptions()
// We should be on the first non-disabled option
assertActiveListboxOption(options[2])
@@ -2571,7 +2571,7 @@ describe('Keyboard interactions', () => {
// We should not be able to go to the end
await press(Keys.Home)
const options = getListboxOptions()
let options = getListboxOptions()
assertActiveListboxOption(options[3])
})
)
@@ -2640,7 +2640,7 @@ describe('Keyboard interactions', () => {
// Open listbox
await press(Keys.ArrowUp)
const options = getListboxOptions()
let options = getListboxOptions()
// We should be on the last option
assertActiveListboxOption(options[2])
@@ -2682,7 +2682,7 @@ describe('Keyboard interactions', () => {
// We should not be able to go to the end
await press(Keys.PageUp)
const options = getListboxOptions()
let options = getListboxOptions()
// We should be on the first non-disabled option
assertActiveListboxOption(options[2])
@@ -2722,7 +2722,7 @@ describe('Keyboard interactions', () => {
// We should not be able to go to the end
await press(Keys.PageUp)
const options = getListboxOptions()
let options = getListboxOptions()
assertActiveListboxOption(options[3])
})
)
@@ -2788,7 +2788,7 @@ describe('Keyboard interactions', () => {
// Open listbox
await click(getListboxButton())
const options = getListboxOptions()
let options = getListboxOptions()
// We should be able to go to the second option
await type(word('bob'))
@@ -2827,7 +2827,7 @@ describe('Keyboard interactions', () => {
// Open listbox
await press(Keys.ArrowUp)
const options = getListboxOptions()
let options = getListboxOptions()
// We should be on the last option
assertActiveListboxOption(options[2])
@@ -2869,7 +2869,7 @@ describe('Keyboard interactions', () => {
// Open listbox
await press(Keys.ArrowUp)
const options = getListboxOptions()
let options = getListboxOptions()
// We should be on the last option
assertActiveListboxOption(options[2])
@@ -2913,7 +2913,7 @@ describe('Keyboard interactions', () => {
// Open listbox
await press(Keys.ArrowUp)
const options = getListboxOptions()
let options = getListboxOptions()
// We should be on the last option
assertActiveListboxOption(options[2])
@@ -3023,7 +3023,7 @@ describe('Mouse interactions', () => {
assertListboxButtonLinkedWithListbox()
// Verify we have listbox options
const options = getListboxOptions()
let options = getListboxOptions()
expect(options).toHaveLength(3)
options.forEach(option => assertListboxOption(option))
})
@@ -3131,7 +3131,7 @@ describe('Mouse interactions', () => {
assertListboxButtonLinkedWithListbox()
// Verify we have listbox options
const options = getListboxOptions()
let options = getListboxOptions()
expect(options).toHaveLength(3)
options.forEach((option, i) => assertListboxOption(option, { selected: i === 1 }))
@@ -3261,7 +3261,7 @@ describe('Mouse interactions', () => {
setup: () => ({ value: ref(null) }),
})
const [button1, button2] = getListboxButtons()
let [button1, button2] = getListboxButtons()
// Click the first menu button
await click(button1)
@@ -3333,7 +3333,7 @@ describe('Mouse interactions', () => {
// Open listbox
await click(getListboxButton())
const options = getListboxOptions()
let options = getListboxOptions()
// We should be able to go to the second option
await mouseMove(options[1])
assertActiveListboxOption(options[1])
@@ -3368,7 +3368,7 @@ describe('Mouse interactions', () => {
// Open listbox
await click(getListboxButton())
const options = getListboxOptions()
let options = getListboxOptions()
// We should be able to go to the second option
await mouseMove(options[1])
assertActiveListboxOption(options[1])
@@ -3395,7 +3395,7 @@ describe('Mouse interactions', () => {
// Open listbox
await click(getListboxButton())
const options = getListboxOptions()
let options = getListboxOptions()
// We should be able to go to the second option
await mouseMove(options[1])
@@ -3430,7 +3430,7 @@ describe('Mouse interactions', () => {
// Open listbox
await click(getListboxButton())
const options = getListboxOptions()
let options = getListboxOptions()
await mouseMove(options[1])
assertNoActiveListboxOption()
@@ -3459,7 +3459,7 @@ describe('Mouse interactions', () => {
// Open listbox
await click(getListboxButton())
const options = getListboxOptions()
let options = getListboxOptions()
// Try to hover over option 1, which is disabled
await mouseMove(options[1])
@@ -3489,7 +3489,7 @@ describe('Mouse interactions', () => {
// Open listbox
await click(getListboxButton())
const options = getListboxOptions()
let options = getListboxOptions()
// We should be able to go to the second option
await mouseMove(options[1])
@@ -3536,7 +3536,7 @@ describe('Mouse interactions', () => {
// Open listbox
await click(getListboxButton())
const options = getListboxOptions()
let options = getListboxOptions()
// Try to hover over option 1, which is disabled
await mouseMove(options[1])
@@ -3550,7 +3550,7 @@ describe('Mouse interactions', () => {
it(
'should be possible to click a listbox option, which closes the listbox',
suppressConsoleLogs(async () => {
const handleChange = jest.fn()
let handleChange = jest.fn()
renderTemplate({
template: `
<Listbox v-model="value">
@@ -3563,7 +3563,7 @@ describe('Mouse interactions', () => {
</Listbox>
`,
setup() {
const value = ref(null)
let value = ref(null)
watch([value], () => handleChange(value.value))
return { value }
},
@@ -3574,7 +3574,7 @@ describe('Mouse interactions', () => {
assertListbox({ state: ListboxState.Visible })
assertActiveElement(getListbox())
const options = getListboxOptions()
let options = getListboxOptions()
// We should be able to click the first option
await click(options[1])
@@ -3596,7 +3596,7 @@ describe('Mouse interactions', () => {
it(
'should be possible to click a disabled listbox option, which is a no-op',
suppressConsoleLogs(async () => {
const handleChange = jest.fn()
let handleChange = jest.fn()
renderTemplate({
template: `
<Listbox v-model="value">
@@ -3611,7 +3611,7 @@ describe('Mouse interactions', () => {
</Listbox>
`,
setup() {
const value = ref(null)
let value = ref(null)
watch([value], () => handleChange(value.value))
return { value }
},
@@ -3622,7 +3622,7 @@ describe('Mouse interactions', () => {
assertListbox({ state: ListboxState.Visible })
assertActiveElement(getListbox())
const options = getListboxOptions()
let options = getListboxOptions()
// We should be able to click the first option
await click(options[1])
@@ -3663,7 +3663,7 @@ describe('Mouse interactions', () => {
assertListbox({ state: ListboxState.Visible })
assertActiveElement(getListbox())
const options = getListboxOptions()
let options = getListboxOptions()
// Verify that nothing is active yet
assertNoActiveListboxOption()
@@ -3698,7 +3698,7 @@ describe('Mouse interactions', () => {
assertListbox({ state: ListboxState.Visible })
assertActiveElement(getListbox())
const options = getListboxOptions()
let options = getListboxOptions()
// We should not be able to focus the first option
await focus(options[1])
@@ -53,13 +53,13 @@ type StateDefinition = {
select(value: unknown): void
}
const ListboxContext = Symbol('ListboxContext') as InjectionKey<StateDefinition>
let ListboxContext = Symbol('ListboxContext') as InjectionKey<StateDefinition>
function useListboxContext(component: string) {
const context = inject(ListboxContext, null)
let context = inject(ListboxContext, null)
if (context === null) {
const err = new Error(`<${component} /> is missing a parent <Listbox /> component.`)
let err = new Error(`<${component} /> is missing a parent <Listbox /> component.`)
if (Error.captureStackTrace) Error.captureStackTrace(err, useListboxContext)
throw err
}
@@ -69,7 +69,7 @@ function useListboxContext(component: string) {
// ---
export const Listbox = defineComponent({
export let Listbox = defineComponent({
name: 'Listbox',
emits: ['update:modelValue'],
props: {
@@ -77,18 +77,18 @@ export const Listbox = defineComponent({
modelValue: { type: [Object, String, Number, Boolean], default: null },
},
setup(props, { slots, attrs, emit }) {
const { modelValue, ...passThroughProps } = props
const listboxState = ref<StateDefinition['listboxState']['value']>(ListboxStates.Closed)
const labelRef = ref<StateDefinition['labelRef']['value']>(null)
const buttonRef = ref<StateDefinition['buttonRef']['value']>(null)
const optionsRef = ref<StateDefinition['optionsRef']['value']>(null)
const options = ref<StateDefinition['options']['value']>([])
const searchQuery = ref<StateDefinition['searchQuery']['value']>('')
const activeOptionIndex = ref<StateDefinition['activeOptionIndex']['value']>(null)
let { modelValue, ...passThroughProps } = props
let listboxState = ref<StateDefinition['listboxState']['value']>(ListboxStates.Closed)
let labelRef = ref<StateDefinition['labelRef']['value']>(null)
let buttonRef = ref<StateDefinition['buttonRef']['value']>(null)
let optionsRef = ref<StateDefinition['optionsRef']['value']>(null)
let options = ref<StateDefinition['options']['value']>([])
let searchQuery = ref<StateDefinition['searchQuery']['value']>('')
let activeOptionIndex = ref<StateDefinition['activeOptionIndex']['value']>(null)
const value = computed(() => props.modelValue)
let value = computed(() => props.modelValue)
const api = {
let api = {
listboxState,
value,
labelRef,
@@ -103,7 +103,7 @@ export const Listbox = defineComponent({
},
openListbox: () => (listboxState.value = ListboxStates.Open),
goToOption(focus: Focus, id?: string) {
const nextActiveOptionIndex = calculateActiveIndex(
let nextActiveOptionIndex = calculateActiveIndex(
focus === Focus.Specific
? { focus: Focus.Specific, id: id! }
: { focus: focus as Exclude<Focus, Focus.Specific> },
@@ -122,7 +122,7 @@ export const Listbox = defineComponent({
search(value: string) {
searchQuery.value += value
const match = options.value.findIndex(
let match = options.value.findIndex(
option =>
!option.dataRef.disabled && option.dataRef.textValue.startsWith(searchQuery.value)
)
@@ -138,10 +138,10 @@ export const Listbox = defineComponent({
options.value.push({ id, dataRef })
},
unregisterOption(id: string) {
const nextOptions = options.value.slice()
const currentActiveOption =
let nextOptions = options.value.slice()
let currentActiveOption =
activeOptionIndex.value !== null ? nextOptions[activeOptionIndex.value] : null
const idx = nextOptions.findIndex(a => a.id === id)
let idx = nextOptions.findIndex(a => a.id === id)
if (idx !== -1) nextOptions.splice(idx, 1)
options.value = nextOptions
activeOptionIndex.value = (() => {
@@ -160,8 +160,8 @@ export const Listbox = defineComponent({
onMounted(() => {
function handler(event: MouseEvent) {
const target = event.target as HTMLElement
const active = document.activeElement
let target = event.target as HTMLElement
let active = document.activeElement
if (listboxState.value !== ListboxStates.Open) return
if (buttonRef.value?.contains(target)) return
@@ -179,7 +179,7 @@ export const Listbox = defineComponent({
provide(ListboxContext, api)
return () => {
const slot = { open: listboxState.value === ListboxStates.Open }
let slot = { open: listboxState.value === ListboxStates.Open }
return render({ props: passThroughProps, slot, slots, attrs })
}
},
@@ -187,14 +187,14 @@ export const Listbox = defineComponent({
// ---
export const ListboxLabel = defineComponent({
export let ListboxLabel = defineComponent({
name: 'ListboxLabel',
props: { as: { type: [Object, String], default: 'label' } },
render() {
const api = useListboxContext('ListboxLabel')
let api = useListboxContext('ListboxLabel')
const slot = { open: api.listboxState.value === ListboxStates.Open }
const propsWeControl = { id: this.id, ref: 'el', onClick: this.handleClick }
let slot = { open: api.listboxState.value === ListboxStates.Open }
let propsWeControl = { id: this.id, ref: 'el', onClick: this.handleClick }
return render({
props: { ...this.$props, ...propsWeControl },
@@ -204,8 +204,8 @@ export const ListboxLabel = defineComponent({
})
},
setup() {
const api = useListboxContext('ListboxLabel')
const id = `headlessui-listbox-label-${useId()}`
let api = useListboxContext('ListboxLabel')
let id = `headlessui-listbox-label-${useId()}`
return {
id,
@@ -219,17 +219,17 @@ export const ListboxLabel = defineComponent({
// ---
export const ListboxButton = defineComponent({
export let ListboxButton = defineComponent({
name: 'ListboxButton',
props: {
disabled: { type: Boolean, default: false },
as: { type: [Object, String], default: 'button' },
},
render() {
const api = useListboxContext('ListboxButton')
let api = useListboxContext('ListboxButton')
const slot = { open: api.listboxState.value === ListboxStates.Open }
const propsWeControl = {
let slot = { open: api.listboxState.value === ListboxStates.Open }
let propsWeControl = {
ref: 'el',
id: this.id,
type: 'button',
@@ -251,8 +251,8 @@ export const ListboxButton = defineComponent({
})
},
setup(props) {
const api = useListboxContext('ListboxButton')
const id = `headlessui-listbox-button-${useId()}`
let api = useListboxContext('ListboxButton')
let id = `headlessui-listbox-button-${useId()}`
function handleKeyDown(event: KeyboardEvent) {
switch (event.key) {
@@ -298,7 +298,7 @@ export const ListboxButton = defineComponent({
// ---
export const ListboxOptions = defineComponent({
export let ListboxOptions = defineComponent({
name: 'ListboxOptions',
props: {
as: { type: [Object, String], default: 'ul' },
@@ -306,10 +306,10 @@ export const ListboxOptions = defineComponent({
unmount: { type: Boolean, default: true },
},
render() {
const api = useListboxContext('ListboxOptions')
let api = useListboxContext('ListboxOptions')
const slot = { open: api.listboxState.value === ListboxStates.Open }
const propsWeControl = {
let slot = { open: api.listboxState.value === ListboxStates.Open }
let propsWeControl = {
'aria-activedescendant':
api.activeOptionIndex.value === null
? undefined
@@ -321,7 +321,7 @@ export const ListboxOptions = defineComponent({
tabIndex: 0,
ref: 'el',
}
const passThroughProps = this.$props
let passThroughProps = this.$props
return render({
props: { ...passThroughProps, ...propsWeControl },
@@ -333,9 +333,9 @@ export const ListboxOptions = defineComponent({
})
},
setup() {
const api = useListboxContext('ListboxOptions')
const id = `headlessui-listbox-options-${useId()}`
const searchDebounce = ref<ReturnType<typeof setTimeout> | null>(null)
let api = useListboxContext('ListboxOptions')
let id = `headlessui-listbox-options-${useId()}`
let searchDebounce = ref<ReturnType<typeof setTimeout> | null>(null)
function handleKeyDown(event: KeyboardEvent) {
if (searchDebounce.value) clearTimeout(searchDebounce.value)
@@ -353,7 +353,7 @@ export const ListboxOptions = defineComponent({
case Keys.Enter:
event.preventDefault()
if (api.activeOptionIndex.value !== null) {
const { dataRef } = api.options.value[api.activeOptionIndex.value]
let { dataRef } = api.options.value[api.activeOptionIndex.value]
api.select(dataRef.value)
}
api.closeListbox()
@@ -400,7 +400,7 @@ export const ListboxOptions = defineComponent({
},
})
export const ListboxOption = defineComponent({
export let ListboxOption = defineComponent({
name: 'ListboxOption',
props: {
as: { type: [Object, String], default: 'li' },
@@ -410,21 +410,21 @@ export const ListboxOption = defineComponent({
className: { type: [String, Function], required: false },
},
setup(props, { slots, attrs }) {
const api = useListboxContext('ListboxOption')
const id = `headlessui-listbox-option-${useId()}`
const { disabled, class: defaultClass, className = defaultClass, value } = props
let api = useListboxContext('ListboxOption')
let id = `headlessui-listbox-option-${useId()}`
let { disabled, class: defaultClass, className = defaultClass, value } = props
const active = computed(() => {
let active = computed(() => {
return api.activeOptionIndex.value !== null
? api.options.value[api.activeOptionIndex.value].id === id
: false
})
const selected = computed(() => toRaw(api.value.value) === toRaw(value))
let selected = computed(() => toRaw(api.value.value) === toRaw(value))
const dataRef = ref<ListboxOptionDataRef['value']>({ disabled, value, textValue: '' })
let dataRef = ref<ListboxOptionDataRef['value']>({ disabled, value, textValue: '' })
onMounted(() => {
const textValue = document
let textValue = document
.getElementById(id)
?.textContent?.toLowerCase()
.trim()
@@ -478,8 +478,8 @@ export const ListboxOption = defineComponent({
}
return () => {
const slot = { active: active.value, selected: selected.value, disabled }
const propsWeControl = {
let slot = { active: active.value, selected: selected.value, disabled }
let propsWeControl = {
id,
role: 'option',
tabIndex: -1,
@@ -40,7 +40,7 @@ beforeAll(() => {
afterAll(() => jest.restoreAllMocks())
function renderTemplate(input: string | Partial<Parameters<typeof defineComponent>[0]>) {
const defaultComponents = { Menu, MenuButton, MenuItems, MenuItem }
let defaultComponents = { Menu, MenuButton, MenuItems, MenuItem }
if (typeof input === 'string') {
return render(defineComponent({ template: input, components: defaultComponents }))
@@ -315,7 +315,7 @@ describe('Rendering', () => {
})
it('should yell when we render MenuItems using a template `as` prop that contains multiple children', async () => {
const state = {
let state = {
resolve(_value: Error | PromiseLike<Error>) {},
done(error: unknown) {
state.resolve(error as Error)
@@ -343,7 +343,7 @@ describe('Rendering', () => {
})
await click(getMenuButton())
const error = await state.promise
let error = await state.promise
expect(error.message).toMatchInlineSnapshot(
`"You should only render 1 child or use the \`as=\\"...\\"\` prop"`
)
@@ -469,7 +469,7 @@ describe('Rendering', () => {
})
it('should yell when we render a MenuItem using a template `as` prop that contains multiple children', async () => {
const state = {
let state = {
resolve(_value: Error | PromiseLike<Error>) {},
done(error: unknown) {
state.resolve(error as Error)
@@ -500,7 +500,7 @@ describe('Rendering', () => {
})
await click(getMenuButton())
const error = await state.promise
let error = await state.promise
expect(error.message).toMatchInlineSnapshot(
`"You should only render 1 child or use the \`as=\\"...\\"\` prop"`
)
@@ -530,7 +530,7 @@ describe('Rendering composition', () => {
// Open menu
await click(getMenuButton())
const items = getMenuItems()
let items = getMenuItems()
// Verify correct classNames
expect('' + items[0].classList).toEqual(JSON.stringify({ active: false, disabled: false }))
@@ -566,7 +566,7 @@ describe('Rendering composition', () => {
it(
'should be possible to swap the menu item with a button for example',
suppressConsoleLogs(async () => {
const MyButton = defineComponent({
let MyButton = defineComponent({
setup(props) {
return () => h('button', { 'data-my-custom-button': true, ...props })
},
@@ -596,7 +596,7 @@ describe('Rendering composition', () => {
await click(getMenuButton())
// Verify items are buttons now
const items = getMenuItems()
let items = getMenuItems()
items.forEach(item =>
assertMenuItem(item, { tag: 'button', attributes: { 'data-my-custom-button': 'true' } })
)
@@ -639,7 +639,7 @@ describe('Keyboard interactions', () => {
assertMenuButtonLinkedWithMenu()
// Verify we have menu items
const items = getMenuItems()
let items = getMenuItems()
expect(items).toHaveLength(3)
items.forEach(item => assertMenuItem(item))
@@ -723,7 +723,7 @@ describe('Keyboard interactions', () => {
// Open menu
await press(Keys.Enter)
const items = getMenuItems()
let items = getMenuItems()
// Verify that the first non-disabled menu item is active
assertMenuLinkedWithMenuItem(items[1])
@@ -753,7 +753,7 @@ describe('Keyboard interactions', () => {
// Open menu
await press(Keys.Enter)
const items = getMenuItems()
let items = getMenuItems()
// Verify that the first non-disabled menu item is active
assertMenuLinkedWithMenuItem(items[2])
@@ -822,7 +822,7 @@ describe('Keyboard interactions', () => {
})
it('should be possible to close the menu with Enter and invoke the active menu item', async () => {
const clickHandler = jest.fn()
let clickHandler = jest.fn()
renderTemplate({
template: `
<Menu>
@@ -850,7 +850,7 @@ describe('Keyboard interactions', () => {
assertMenuButton({ state: MenuState.Visible })
// Activate the first menu item
const items = getMenuItems()
let items = getMenuItems()
await mouseMove(items[0])
// Close menu, and invoke the item
@@ -869,7 +869,7 @@ describe('Keyboard interactions', () => {
})
it('should be possible to use a button as a menu item and invoke it upon Enter', async () => {
const clickHandler = jest.fn()
let clickHandler = jest.fn()
renderTemplate({
template: `
@@ -902,7 +902,7 @@ describe('Keyboard interactions', () => {
assertMenuButton({ state: MenuState.Visible })
// Activate the second menu item
const items = getMenuItems()
let items = getMenuItems()
await mouseMove(items[1])
// Close menu, and invoke the item
@@ -968,7 +968,7 @@ describe('Keyboard interactions', () => {
assertMenuButtonLinkedWithMenu()
// Verify we have menu items
const items = getMenuItems()
let items = getMenuItems()
expect(items).toHaveLength(3)
items.forEach(item => assertMenuItem(item))
assertMenuLinkedWithMenuItem(items[0])
@@ -1050,7 +1050,7 @@ describe('Keyboard interactions', () => {
// Open menu
await press(Keys.Space)
const items = getMenuItems()
let items = getMenuItems()
// Verify that the first non-disabled menu item is active
assertMenuLinkedWithMenuItem(items[1])
@@ -1080,7 +1080,7 @@ describe('Keyboard interactions', () => {
// Open menu
await press(Keys.Space)
const items = getMenuItems()
let items = getMenuItems()
// Verify that the first non-disabled menu item is active
assertMenuLinkedWithMenuItem(items[2])
@@ -1154,7 +1154,7 @@ describe('Keyboard interactions', () => {
it(
'should be possible to close the menu with Space and invoke the active menu item',
suppressConsoleLogs(async () => {
const clickHandler = jest.fn()
let clickHandler = jest.fn()
renderTemplate({
template: `
<Menu>
@@ -1182,7 +1182,7 @@ describe('Keyboard interactions', () => {
assertMenuButton({ state: MenuState.Visible })
// Activate the first menu item
const items = getMenuItems()
let items = getMenuItems()
await mouseMove(items[0])
// Close menu, and invoke the item
@@ -1274,7 +1274,7 @@ describe('Keyboard interactions', () => {
assertMenuButtonLinkedWithMenu()
// Verify we have menu items
const items = getMenuItems()
let items = getMenuItems()
expect(items).toHaveLength(3)
items.forEach(item => assertMenuItem(item))
assertMenuLinkedWithMenuItem(items[0])
@@ -1320,7 +1320,7 @@ describe('Keyboard interactions', () => {
assertMenuButtonLinkedWithMenu()
// Verify we have menu items
const items = getMenuItems()
let items = getMenuItems()
expect(items).toHaveLength(3)
items.forEach(item => assertMenuItem(item))
assertMenuLinkedWithMenuItem(items[0])
@@ -1368,7 +1368,7 @@ describe('Keyboard interactions', () => {
assertMenuButtonLinkedWithMenu()
// Verify we have menu items
const items = getMenuItems()
let items = getMenuItems()
expect(items).toHaveLength(3)
items.forEach(item => assertMenuItem(item))
@@ -1453,7 +1453,7 @@ describe('Keyboard interactions', () => {
await press(Keys.Enter)
// Verify we have menu items
const items = getMenuItems()
let items = getMenuItems()
expect(items).toHaveLength(3)
items.forEach(item => assertMenuItem(item))
assertMenuLinkedWithMenuItem(items[0])
@@ -1496,7 +1496,7 @@ describe('Keyboard interactions', () => {
await press(Keys.Enter)
// Verify we have menu items
const items = getMenuItems()
let items = getMenuItems()
expect(items).toHaveLength(3)
items.forEach(item => assertMenuItem(item))
assertMenuLinkedWithMenuItem(items[1])
@@ -1531,7 +1531,7 @@ describe('Keyboard interactions', () => {
await press(Keys.Enter)
// Verify we have menu items
const items = getMenuItems()
let items = getMenuItems()
expect(items).toHaveLength(3)
items.forEach(item => assertMenuItem(item))
assertMenuLinkedWithMenuItem(items[2])
@@ -1572,7 +1572,7 @@ describe('Keyboard interactions', () => {
assertMenuButtonLinkedWithMenu()
// Verify we have menu items
const items = getMenuItems()
let items = getMenuItems()
expect(items).toHaveLength(3)
items.forEach(item => assertMenuItem(item))
@@ -1625,7 +1625,7 @@ describe('Keyboard interactions', () => {
await press(Keys.ArrowUp)
// Verify we have menu items
const items = getMenuItems()
let items = getMenuItems()
expect(items).toHaveLength(3)
items.forEach(item => assertMenuItem(item))
assertMenuLinkedWithMenuItem(items[0])
@@ -1656,7 +1656,7 @@ describe('Keyboard interactions', () => {
await press(Keys.Enter)
// Verify we have menu items
const items = getMenuItems()
let items = getMenuItems()
expect(items).toHaveLength(3)
items.forEach(item => assertMenuItem(item))
assertMenuLinkedWithMenuItem(items[2])
@@ -1703,7 +1703,7 @@ describe('Keyboard interactions', () => {
assertMenuButtonLinkedWithMenu()
// Verify we have menu items
const items = getMenuItems()
let items = getMenuItems()
expect(items).toHaveLength(3)
items.forEach(item => assertMenuItem(item))
assertMenuLinkedWithMenuItem(items[2])
@@ -1741,7 +1741,7 @@ describe('Keyboard interactions', () => {
// Open menu
await press(Keys.Enter)
const items = getMenuItems()
let items = getMenuItems()
// We should be on the first item
assertMenuLinkedWithMenuItem(items[0])
@@ -1770,7 +1770,7 @@ describe('Keyboard interactions', () => {
// Open menu
await press(Keys.Enter)
const items = getMenuItems()
let items = getMenuItems()
// We should be on the first item
assertMenuLinkedWithMenuItem(items[0])
@@ -1802,7 +1802,7 @@ describe('Keyboard interactions', () => {
// We should not be able to go to the end
await press(Keys.End)
const items = getMenuItems()
let items = getMenuItems()
assertMenuLinkedWithMenuItem(items[0])
})
@@ -1851,7 +1851,7 @@ describe('Keyboard interactions', () => {
// Open menu
await press(Keys.Enter)
const items = getMenuItems()
let items = getMenuItems()
// We should be on the first item
assertMenuLinkedWithMenuItem(items[0])
@@ -1880,7 +1880,7 @@ describe('Keyboard interactions', () => {
// Open menu
await press(Keys.Enter)
const items = getMenuItems()
let items = getMenuItems()
// We should be on the first item
assertMenuLinkedWithMenuItem(items[0])
@@ -1912,7 +1912,7 @@ describe('Keyboard interactions', () => {
// We should not be able to go to the end
await press(Keys.PageDown)
const items = getMenuItems()
let items = getMenuItems()
assertMenuLinkedWithMenuItem(items[0])
})
@@ -1961,7 +1961,7 @@ describe('Keyboard interactions', () => {
// Open menu
await press(Keys.ArrowUp)
const items = getMenuItems()
let items = getMenuItems()
// We should be on the last item
assertMenuLinkedWithMenuItem(items[2])
@@ -1993,7 +1993,7 @@ describe('Keyboard interactions', () => {
// We should not be able to go to the end
await press(Keys.Home)
const items = getMenuItems()
let items = getMenuItems()
// We should be on the first non-disabled item
assertMenuLinkedWithMenuItem(items[2])
@@ -2021,7 +2021,7 @@ describe('Keyboard interactions', () => {
// We should not be able to go to the end
await press(Keys.Home)
const items = getMenuItems()
let items = getMenuItems()
assertMenuLinkedWithMenuItem(items[3])
})
@@ -2070,7 +2070,7 @@ describe('Keyboard interactions', () => {
// Open menu
await press(Keys.ArrowUp)
const items = getMenuItems()
let items = getMenuItems()
// We should be on the last item
assertMenuLinkedWithMenuItem(items[2])
@@ -2102,7 +2102,7 @@ describe('Keyboard interactions', () => {
// We should not be able to go to the end
await press(Keys.PageUp)
const items = getMenuItems()
let items = getMenuItems()
// We should be on the first non-disabled item
assertMenuLinkedWithMenuItem(items[2])
@@ -2130,7 +2130,7 @@ describe('Keyboard interactions', () => {
// We should not be able to go to the end
await press(Keys.PageUp)
const items = getMenuItems()
let items = getMenuItems()
assertMenuLinkedWithMenuItem(items[3])
})
@@ -2176,7 +2176,7 @@ describe('Keyboard interactions', () => {
// Open menu
await click(getMenuButton())
const items = getMenuItems()
let items = getMenuItems()
// We should be able to go to the second item
await type(word('bob'))
@@ -2209,7 +2209,7 @@ describe('Keyboard interactions', () => {
// Open menu
await press(Keys.ArrowUp)
const items = getMenuItems()
let items = getMenuItems()
// We should be on the last item
assertMenuLinkedWithMenuItem(items[2])
@@ -2247,7 +2247,7 @@ describe('Keyboard interactions', () => {
// Open menu
await press(Keys.ArrowUp)
const items = getMenuItems()
let items = getMenuItems()
// We should be on the last item
assertMenuLinkedWithMenuItem(items[2])
@@ -2284,7 +2284,7 @@ describe('Keyboard interactions', () => {
// Open menu
await press(Keys.ArrowUp)
const items = getMenuItems()
let items = getMenuItems()
// We should be on the last item
assertMenuLinkedWithMenuItem(items[2])
@@ -2329,7 +2329,7 @@ describe('Mouse interactions', () => {
assertMenuButtonLinkedWithMenu()
// Verify we have menu items
const items = getMenuItems()
let items = getMenuItems()
expect(items).toHaveLength(3)
items.forEach(item => assertMenuItem(item))
})
@@ -2516,7 +2516,7 @@ describe('Mouse interactions', () => {
</div>
`)
const [button1, button2] = getMenuButtons()
let [button1, button2] = getMenuButtons()
// Click the first menu button
await click(button1)
@@ -2550,7 +2550,7 @@ describe('Mouse interactions', () => {
// Open menu
await click(getMenuButton())
const items = getMenuItems()
let items = getMenuItems()
// We should be able to go to the second item
await mouseMove(items[1])
assertMenuLinkedWithMenuItem(items[1])
@@ -2579,7 +2579,7 @@ describe('Mouse interactions', () => {
// Open menu
await click(getMenuButton())
const items = getMenuItems()
let items = getMenuItems()
// We should be able to go to the second item
await mouseMove(items[1])
assertMenuLinkedWithMenuItem(items[1])
@@ -2600,7 +2600,7 @@ describe('Mouse interactions', () => {
// Open menu
await click(getMenuButton())
const items = getMenuItems()
let items = getMenuItems()
// We should be able to go to the second item
await mouseMove(items[1])
@@ -2627,7 +2627,7 @@ describe('Mouse interactions', () => {
// Open menu
await click(getMenuButton())
const items = getMenuItems()
let items = getMenuItems()
await mouseMove(items[1])
assertNoActiveMenuItem()
@@ -2648,7 +2648,7 @@ describe('Mouse interactions', () => {
// Open menu
await click(getMenuButton())
const items = getMenuItems()
let items = getMenuItems()
// Try to hover over item 1, which is disabled
await mouseMove(items[1])
@@ -2672,7 +2672,7 @@ describe('Mouse interactions', () => {
// Open menu
await click(getMenuButton())
const items = getMenuItems()
let items = getMenuItems()
// We should be able to go to the second item
await mouseMove(items[1])
@@ -2711,7 +2711,7 @@ describe('Mouse interactions', () => {
// Open menu
await click(getMenuButton())
const items = getMenuItems()
let items = getMenuItems()
// Try to hover over item 1, which is disabled
await mouseMove(items[1])
@@ -2722,7 +2722,7 @@ describe('Mouse interactions', () => {
})
it('should be possible to click a menu item, which closes the menu', async () => {
const clickHandler = jest.fn()
let clickHandler = jest.fn()
renderTemplate({
template: `
<Menu>
@@ -2741,7 +2741,7 @@ describe('Mouse interactions', () => {
await click(getMenuButton())
assertMenu({ state: MenuState.Visible })
const items = getMenuItems()
let items = getMenuItems()
// We should be able to click the first item
await click(items[1])
@@ -2751,7 +2751,7 @@ describe('Mouse interactions', () => {
})
it('should be possible to click a menu item, which closes the menu and invokes the @click handler', async () => {
const clickHandler = jest.fn()
let clickHandler = jest.fn()
renderTemplate({
template: `
<Menu>
@@ -2806,7 +2806,7 @@ describe('Mouse interactions', () => {
await click(getMenuButton())
assertMenu({ state: MenuState.Visible })
const items = getMenuItems()
let items = getMenuItems()
// We should be able to click the first item
await click(items[1])
@@ -2829,7 +2829,7 @@ describe('Mouse interactions', () => {
await click(getMenuButton())
assertMenu({ state: MenuState.Visible })
const items = getMenuItems()
let items = getMenuItems()
// Verify that nothing is active yet
assertNoActiveMenuItem()
@@ -2855,7 +2855,7 @@ describe('Mouse interactions', () => {
await click(getMenuButton())
assertMenu({ state: MenuState.Visible })
const items = getMenuItems()
let items = getMenuItems()
// We should not be able to focus the first item
await focus(items[1])
@@ -2863,7 +2863,7 @@ describe('Mouse interactions', () => {
})
it('should not be possible to activate a disabled item', async () => {
const clickHandler = jest.fn()
let clickHandler = jest.fn()
renderTemplate({
template: `
@@ -2887,7 +2887,7 @@ describe('Mouse interactions', () => {
await click(getMenuButton())
assertMenu({ state: MenuState.Visible })
const items = getMenuItems()
let items = getMenuItems()
await focus(items[0])
await focus(items[1])
@@ -45,13 +45,13 @@ type StateDefinition = {
unregisterItem(id: string): void
}
const MenuContext = Symbol('MenuContext') as InjectionKey<StateDefinition>
let MenuContext = Symbol('MenuContext') as InjectionKey<StateDefinition>
function useMenuContext(component: string) {
const context = inject(MenuContext, null)
let context = inject(MenuContext, null)
if (context === null) {
const err = new Error(`<${component} /> is missing a parent <Menu /> component.`)
let err = new Error(`<${component} /> is missing a parent <Menu /> component.`)
if (Error.captureStackTrace) Error.captureStackTrace(err, useMenuContext)
throw err
}
@@ -59,17 +59,17 @@ function useMenuContext(component: string) {
return context
}
export const Menu = defineComponent({
export let Menu = defineComponent({
props: { as: { type: [Object, String], default: 'template' } },
setup(props, { slots, attrs }) {
const menuState = ref<StateDefinition['menuState']['value']>(MenuStates.Closed)
const buttonRef = ref<StateDefinition['buttonRef']['value']>(null)
const itemsRef = ref<StateDefinition['itemsRef']['value']>(null)
const items = ref<StateDefinition['items']['value']>([])
const searchQuery = ref<StateDefinition['searchQuery']['value']>('')
const activeItemIndex = ref<StateDefinition['activeItemIndex']['value']>(null)
let menuState = ref<StateDefinition['menuState']['value']>(MenuStates.Closed)
let buttonRef = ref<StateDefinition['buttonRef']['value']>(null)
let itemsRef = ref<StateDefinition['itemsRef']['value']>(null)
let items = ref<StateDefinition['items']['value']>([])
let searchQuery = ref<StateDefinition['searchQuery']['value']>('')
let activeItemIndex = ref<StateDefinition['activeItemIndex']['value']>(null)
const api = {
let api = {
menuState,
buttonRef,
itemsRef,
@@ -82,7 +82,7 @@ export const Menu = defineComponent({
},
openMenu: () => (menuState.value = MenuStates.Open),
goToItem(focus: Focus, id?: string) {
const nextActiveItemIndex = calculateActiveIndex(
let nextActiveItemIndex = calculateActiveIndex(
focus === Focus.Specific
? { focus: Focus.Specific, id: id! }
: { focus: focus as Exclude<Focus, Focus.Specific> },
@@ -101,7 +101,7 @@ export const Menu = defineComponent({
search(value: string) {
searchQuery.value += value
const match = items.value.findIndex(
let match = items.value.findIndex(
item => item.dataRef.textValue.startsWith(searchQuery.value) && !item.dataRef.disabled
)
@@ -117,10 +117,10 @@ export const Menu = defineComponent({
items.value.push({ id, dataRef })
},
unregisterItem(id: string) {
const nextItems = items.value.slice()
const currentActiveItem =
let nextItems = items.value.slice()
let currentActiveItem =
activeItemIndex.value !== null ? nextItems[activeItemIndex.value] : null
const idx = nextItems.findIndex(a => a.id === id)
let idx = nextItems.findIndex(a => a.id === id)
if (idx !== -1) nextItems.splice(idx, 1)
items.value = nextItems
activeItemIndex.value = (() => {
@@ -136,8 +136,8 @@ export const Menu = defineComponent({
onMounted(() => {
function handler(event: MouseEvent) {
const target = event.target as HTMLElement
const active = document.activeElement
let target = event.target as HTMLElement
let active = document.activeElement
if (menuState.value !== MenuStates.Open) return
if (buttonRef.value?.contains(target)) return
@@ -155,22 +155,22 @@ export const Menu = defineComponent({
provide(MenuContext, api)
return () => {
const slot = { open: menuState.value === MenuStates.Open }
let slot = { open: menuState.value === MenuStates.Open }
return render({ props, slot, slots, attrs })
}
},
})
export const MenuButton = defineComponent({
export let MenuButton = defineComponent({
props: {
disabled: { type: Boolean, default: false },
as: { type: [Object, String], default: 'button' },
},
render() {
const api = useMenuContext('MenuButton')
let api = useMenuContext('MenuButton')
const slot = { open: api.menuState.value === MenuStates.Open }
const propsWeControl = {
let slot = { open: api.menuState.value === MenuStates.Open }
let propsWeControl = {
ref: 'el',
id: this.id,
type: 'button',
@@ -189,8 +189,8 @@ export const MenuButton = defineComponent({
})
},
setup(props) {
const api = useMenuContext('MenuButton')
const id = `headlessui-menu-button-${useId()}`
let api = useMenuContext('MenuButton')
let id = `headlessui-menu-button-${useId()}`
function handleKeyDown(event: KeyboardEvent) {
switch (event.key) {
@@ -239,17 +239,17 @@ export const MenuButton = defineComponent({
},
})
export const MenuItems = defineComponent({
export let MenuItems = defineComponent({
props: {
as: { type: [Object, String], default: 'div' },
static: { type: Boolean, default: false },
unmount: { type: Boolean, default: true },
},
render() {
const api = useMenuContext('MenuItems')
let api = useMenuContext('MenuItems')
const slot = { open: api.menuState.value === MenuStates.Open }
const propsWeControl = {
let slot = { open: api.menuState.value === MenuStates.Open }
let propsWeControl = {
'aria-activedescendant':
api.activeItemIndex.value === null
? undefined
@@ -261,7 +261,7 @@ export const MenuItems = defineComponent({
tabIndex: 0,
ref: 'el',
}
const passThroughProps = this.$props
let passThroughProps = this.$props
return render({
props: { ...passThroughProps, ...propsWeControl },
@@ -273,9 +273,9 @@ export const MenuItems = defineComponent({
})
},
setup() {
const api = useMenuContext('MenuItems')
const id = `headlessui-menu-items-${useId()}`
const searchDebounce = ref<ReturnType<typeof setTimeout> | null>(null)
let api = useMenuContext('MenuItems')
let id = `headlessui-menu-items-${useId()}`
let searchDebounce = ref<ReturnType<typeof setTimeout> | null>(null)
function handleKeyDown(event: KeyboardEvent) {
if (searchDebounce.value) clearTimeout(searchDebounce.value)
@@ -293,7 +293,7 @@ export const MenuItems = defineComponent({
case Keys.Enter:
event.preventDefault()
if (api.activeItemIndex.value !== null) {
const { id } = api.items.value[api.activeItemIndex.value]
let { id } = api.items.value[api.activeItemIndex.value]
document.getElementById(id)?.click()
}
api.closeMenu()
@@ -340,7 +340,7 @@ export const MenuItems = defineComponent({
},
})
export const MenuItem = defineComponent({
export let MenuItem = defineComponent({
props: {
as: { type: [Object, String], default: 'template' },
disabled: { type: Boolean, default: false },
@@ -348,19 +348,19 @@ export const MenuItem = defineComponent({
className: { type: [String, Function], required: false },
},
setup(props, { slots, attrs }) {
const api = useMenuContext('MenuItem')
const id = `headlessui-menu-item-${useId()}`
const { disabled, class: defaultClass, className = defaultClass } = props
let api = useMenuContext('MenuItem')
let id = `headlessui-menu-item-${useId()}`
let { disabled, class: defaultClass, className = defaultClass } = props
const active = computed(() => {
let active = computed(() => {
return api.activeItemIndex.value !== null
? api.items.value[api.activeItemIndex.value].id === id
: false
})
const dataRef = ref<MenuItemDataRef['value']>({ disabled, textValue: '' })
let dataRef = ref<MenuItemDataRef['value']>({ disabled, textValue: '' })
onMounted(() => {
const textValue = document
let textValue = document
.getElementById(id)
?.textContent?.toLowerCase()
.trim()
@@ -394,8 +394,8 @@ export const MenuItem = defineComponent({
}
return () => {
const slot = { active: active.value, disabled }
const propsWeControl = {
let slot = { active: active.value, disabled }
let propsWeControl = {
id,
role: 'menuitem',
tabIndex: -1,
@@ -15,7 +15,7 @@ import { suppressConsoleLogs } from '../../test-utils/suppress-console-logs'
jest.mock('../../hooks/use-id')
function renderTemplate(input: string | Partial<Parameters<typeof defineComponent>[0]>) {
const defaultComponents = { Switch, SwitchLabel, SwitchGroup }
let defaultComponents = { Switch, SwitchLabel, SwitchGroup }
if (typeof input === 'string') {
return render(defineComponent({ template: input, components: defaultComponents }))
@@ -170,11 +170,11 @@ describe('Render composition', () => {
describe('Keyboard interactions', () => {
describe('`Space` key', () => {
it('should be possible to toggle the Switch with Space', async () => {
const handleChange = jest.fn()
let handleChange = jest.fn()
renderTemplate({
template: `<Switch v-model="checked" />`,
setup() {
const checked = ref(false)
let checked = ref(false)
watch([checked], () => handleChange(checked.value))
return { checked }
},
@@ -202,11 +202,11 @@ describe('Keyboard interactions', () => {
describe('`Enter` key', () => {
it('should not be possible to use Enter to toggle the Switch', async () => {
const handleChange = jest.fn()
let handleChange = jest.fn()
renderTemplate({
template: `<Switch v-model="checked" />`,
setup() {
const checked = ref(false)
let checked = ref(false)
watch([checked], () => handleChange(checked.value))
return { checked }
},
@@ -257,11 +257,11 @@ describe('Keyboard interactions', () => {
describe('Mouse interactions', () => {
it('should be possible to toggle the Switch with a click', async () => {
const handleChange = jest.fn()
let handleChange = jest.fn()
renderTemplate({
template: `<Switch v-model="checked" />`,
setup() {
const checked = ref(false)
let checked = ref(false)
watch([checked], () => handleChange(checked.value))
return { checked }
},
@@ -284,7 +284,7 @@ describe('Mouse interactions', () => {
})
it('should be possible to toggle the Switch with a click on the Label', async () => {
const handleChange = jest.fn()
let handleChange = jest.fn()
renderTemplate({
template: `
<SwitchGroup>
@@ -293,7 +293,7 @@ describe('Mouse interactions', () => {
</SwitchGroup>
`,
setup() {
const checked = ref(false)
let checked = ref(false)
watch([checked], () => handleChange(checked.value))
return { checked }
},
@@ -11,13 +11,13 @@ type StateDefinition = {
labelRef: Ref<HTMLLabelElement | null>
}
const GroupContext = Symbol('GroupContext') as InjectionKey<StateDefinition>
let GroupContext = Symbol('GroupContext') as InjectionKey<StateDefinition>
function useGroupContext(component: string) {
const context = inject(GroupContext, null)
let context = inject(GroupContext, null)
if (context === null) {
const err = new Error(`<${component} /> is missing a parent <SwitchGroup /> component.`)
let err = new Error(`<${component} /> is missing a parent <SwitchGroup /> component.`)
if (Error.captureStackTrace) Error.captureStackTrace(err, useGroupContext)
throw err
}
@@ -27,16 +27,16 @@ function useGroupContext(component: string) {
// ---
export const SwitchGroup = defineComponent({
export let SwitchGroup = defineComponent({
name: 'SwitchGroup',
props: {
as: { type: [Object, String], default: 'template' },
},
setup(props, { slots, attrs }) {
const switchRef = ref<StateDefinition['switchRef']['value']>(null)
const labelRef = ref<StateDefinition['labelRef']['value']>(null)
let switchRef = ref<StateDefinition['switchRef']['value']>(null)
let labelRef = ref<StateDefinition['labelRef']['value']>(null)
const api = { switchRef, labelRef }
let api = { switchRef, labelRef }
provide(GroupContext, api)
@@ -46,7 +46,7 @@ export const SwitchGroup = defineComponent({
// ---
export const Switch = defineComponent({
export let Switch = defineComponent({
name: 'Switch',
emits: ['update:modelValue'],
props: {
@@ -56,13 +56,13 @@ export const Switch = defineComponent({
className: { type: [String, Function], required: false },
},
render() {
const api = inject(GroupContext, null)
const { class: defaultClass, className = defaultClass } = this.$props
let api = inject(GroupContext, null)
let { class: defaultClass, className = defaultClass } = this.$props
const labelledby = computed(() => api?.labelRef.value?.id)
let labelledby = computed(() => api?.labelRef.value?.id)
const slot = { checked: this.$props.modelValue }
const propsWeControl = {
let slot = { checked: this.$props.modelValue }
let propsWeControl = {
id: this.id,
ref: api === null ? undefined : api.switchRef,
role: 'switch',
@@ -87,8 +87,8 @@ export const Switch = defineComponent({
})
},
setup(props, { emit }) {
const api = inject(GroupContext, null)
const id = `headlessui-switch-${useId()}`
let api = inject(GroupContext, null)
let id = `headlessui-switch-${useId()}`
function toggle() {
emit('update:modelValue', !props.modelValue)
@@ -115,11 +115,11 @@ export const Switch = defineComponent({
// ---
export const SwitchLabel = defineComponent({
export let SwitchLabel = defineComponent({
name: 'SwitchLabel',
props: { as: { type: [Object, String], default: 'label' } },
render() {
const propsWeControl = {
let propsWeControl = {
id: this.id,
ref: 'el',
onClick: this.handleClick,
@@ -133,8 +133,8 @@ export const SwitchLabel = defineComponent({
})
},
setup() {
const api = useGroupContext('SwitchLabel')
const id = `headlessui-switch-label-${useId()}`
let api = useGroupContext('SwitchLabel')
let id = `headlessui-switch-label-${useId()}`
return {
id,
@@ -580,7 +580,7 @@ export function assertLabelValue(element: HTMLElement | null, value: string) {
if (element === null) return expect(element).not.toBe(null)
if (element.hasAttribute('aria-labelledby')) {
const ids = element.getAttribute('aria-labelledby')!.split(' ')
let ids = element.getAttribute('aria-labelledby')!.split(' ')
expect(ids.map(id => document.getElementById(id)?.textContent).join(' ')).toEqual(value)
return
}
@@ -7,7 +7,7 @@ type Events = 'onKeyDown' | 'onKeyUp' | 'onKeyPress' | 'onClick' | 'onBlur' | 'o
let events: Events[] = ['onKeyDown', 'onKeyUp', 'onKeyPress', 'onClick', 'onBlur', 'onFocus']
function renderTemplate(input: string | Partial<Parameters<typeof defineComponent>[0]>) {
const defaultComponents = {}
let defaultComponents = {}
if (typeof input === 'string') {
return render(defineComponent({ template: input, components: defaultComponents }))
@@ -8,7 +8,7 @@ function nextFrame(cb: Function): void {
)
}
export const Keys: Record<string, Partial<KeyboardEvent>> = {
export let Keys: Record<string, Partial<KeyboardEvent>> = {
Space: { key: ' ', keyCode: 32, charCode: 32 },
Enter: { key: 'Enter', keyCode: 13, charCode: 13 },
Escape: { key: 'Escape', keyCode: 27, charCode: 27 },
@@ -277,13 +277,13 @@ export async function mouseLeave(element: Document | Element | Window | null) {
// ---
function focusNext(event: Partial<KeyboardEvent>) {
const direction = event.shiftKey ? -1 : +1
const focusableElements = getFocusableElements()
const total = focusableElements.length
let direction = event.shiftKey ? -1 : +1
let focusableElements = getFocusableElements()
let total = focusableElements.length
function innerFocusNext(offset = 0): Element {
const currentIdx = focusableElements.indexOf(document.activeElement as HTMLElement)
const next = focusableElements[(currentIdx + total + direction + offset) % total] as HTMLElement
let currentIdx = focusableElements.indexOf(document.activeElement as HTMLElement)
let next = focusableElements[(currentIdx + total + direction + offset) % total] as HTMLElement
if (next) next?.focus({ preventScroll: true })
@@ -296,7 +296,7 @@ function focusNext(event: Partial<KeyboardEvent>) {
// Credit:
// - https://stackoverflow.com/a/30753870
const focusableSelector = [
let focusableSelector = [
'[contentEditable=true]',
'[tabindex]',
'a[href]',
@@ -8,7 +8,7 @@ export function suppressConsoleLogs<T extends unknown[]>(
type: FunctionPropertyNames<typeof global.console> = 'warn'
) {
return (...args: T) => {
const spy = jest.spyOn(global.console, type).mockImplementation(jest.fn())
let spy = jest.spyOn(global.console, type).mockImplementation(jest.fn())
return new Promise<void>((resolve, reject) => {
Promise.resolve(cb(...args)).then(resolve, reject)
@@ -1,20 +1,20 @@
import { mount } from '@vue/test-utils'
import { logDOM, fireEvent } from '@testing-library/dom'
const mountedWrappers = new Set()
let mountedWrappers = new Set()
export function render(
TestComponent: any,
options?: Omit<Parameters<typeof mount>[1], 'attachTo'>
) {
const div = document.createElement('div')
const baseElement = document.body
const container = baseElement.appendChild(div)
let div = document.createElement('div')
let baseElement = document.body
let container = baseElement.appendChild(div)
const attachTo = document.createElement('div')
let attachTo = document.createElement('div')
container.appendChild(attachTo)
const wrapper = mount(TestComponent, {
let wrapper = mount(TestComponent, {
...options,
attachTo,
})
@@ -31,19 +31,19 @@ export function calculateActiveIndex<TItem>(
resolveDisabled(item: TItem): boolean
}
) {
const items = resolvers.resolveItems()
let items = resolvers.resolveItems()
if (items.length <= 0) return null
const currentActiveIndex = resolvers.resolveActiveIndex()
const activeIndex = currentActiveIndex ?? -1
let currentActiveIndex = resolvers.resolveActiveIndex()
let activeIndex = currentActiveIndex ?? -1
const nextActiveIndex = (() => {
let nextActiveIndex = (() => {
switch (action.focus) {
case Focus.First:
return items.findIndex(item => !resolvers.resolveDisabled(item))
case Focus.Previous: {
const idx = items
let idx = items
.slice()
.reverse()
.findIndex((item, idx, all) => {
@@ -61,7 +61,7 @@ export function calculateActiveIndex<TItem>(
})
case Focus.Last: {
const idx = items
let idx = items
.slice()
.reverse()
.findIndex(item => !resolvers.resolveDisabled(item))
+2 -2
View File
@@ -4,11 +4,11 @@ export function match<TValue extends string | number = string, TReturnValue = un
...args: any[]
): TReturnValue {
if (value in lookup) {
const returnValue = lookup[value]
let returnValue = lookup[value]
return typeof returnValue === 'function' ? returnValue(...args) : returnValue
}
const error = new Error(
let error = new Error(
`Tried to handle "${value}" but there is no handler defined. Only defined handlers are: ${Object.keys(
lookup
)
+4 -4
View File
@@ -48,7 +48,7 @@ export function render({
}
if (features & Features.RenderStrategy) {
const strategy = main.props.unmount ?? true ? RenderStrategy.Unmount : RenderStrategy.Hidden
let strategy = main.props.unmount ?? true ? RenderStrategy.Unmount : RenderStrategy.Hidden
return match(strategy, {
[RenderStrategy.Unmount]() {
@@ -78,13 +78,13 @@ function _render({
attrs: Record<string, any>
slots: Slots
}) {
const { as, ...passThroughProps } = omit(props, ['unmount', 'static'])
let { as, ...passThroughProps } = omit(props, ['unmount', 'static'])
const children = slots.default?.(slot)
let children = slots.default?.(slot)
if (as === 'template') {
if (Object.keys(passThroughProps).length > 0 || 'class' in attrs) {
const [firstChild, ...other] = children ?? []
let [firstChild, ...other] = children ?? []
if (other.length > 0)
throw new Error('You should only render 1 child or use the `as="..."` prop')
+14 -14
View File
@@ -1,11 +1,11 @@
const fs = require('fs')
const path = require('path')
let fs = require('fs')
let path = require('path')
const prettier = require('prettier')
const Prism = require('prismjs')
let prettier = require('prettier')
let Prism = require('prismjs')
require('prismjs/plugins/custom-class/prism-custom-class')
const routes = require('./examples/src/routes')
let routes = require('./examples/src/routes')
function flatten(routes, resolver) {
return routes
@@ -17,10 +17,10 @@ function flatten(routes, resolver) {
// file. However just doing dynamic imports() doesn't work well at build time. Therefore we will
// generate a fake file that contains them all.
let i = 0
const map = {}
const contents = flatten(routes, route => route.component)
let map = {}
let contents = flatten(routes, route => route.component)
.map(path => {
const name = `Component$${++i}`
let name = `Component$${++i}`
map[path] = name
return `import ${name} from ".${path}";`
})
@@ -53,7 +53,7 @@ Prism.plugins.customClass.map({
comment: 'text-gray-400 italic',
})
const sourcePipeline = pipe(
let sourcePipeline = pipe(
path => fs.readFileSync(path, 'utf8'),
contents =>
prettier.format(contents, {
@@ -74,8 +74,8 @@ const sourcePipeline = pipe(
].join('')
)
const skipRoutes = ['/']
const source = Object.assign(
let skipRoutes = ['/']
let source = Object.assign(
{},
...flatten(routes, route => ({
urlPath: route.path,
@@ -93,14 +93,14 @@ fs.writeFileSync(
)
// ---
const TailwindUIPlugin = ({
let HeadlessUIPlugin = ({
root, // project root directory, absolute path
app, // Koa app instance
server, // raw http server instance
watcher, // chokidar file watcher instance
resolver, // chokidar file watcher instance
}) => {
const routePaths = flatten(routes, route => route.path)
let routePaths = flatten(routes, route => route.path)
app.use(async (ctx, next) => {
if (routePaths.includes(ctx.path)) {
@@ -118,5 +118,5 @@ module.exports = {
'./src/index.ts'
),
},
configureServer: [TailwindUIPlugin],
configureServer: [HeadlessUIPlugin],
}