diff --git a/jest/custom-matchers.ts b/jest/custom-matchers.ts index 7c16750..56c7e80 100644 --- a/jest/custom-matchers.ts +++ b/jest/custom-matchers.ts @@ -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 diff --git a/packages/@headlessui-react/pages/_app.tsx b/packages/@headlessui-react/pages/_app.tsx index 222a97a..2462d47 100644 --- a/packages/@headlessui-react/pages/_app.tsx +++ b/packages/@headlessui-react/pages/_app.tsx @@ -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 ( {children} @@ -58,23 +58,23 @@ function tap(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([]) - const d = useDisposables() - const KeyDisplay = useKeyDisplay() + let [keys, setKeys] = useState([]) + let d = useDisposables() + let KeyDisplay = useKeyDisplay() - React.useEffect(() => { + useEffect(() => { function handler(event: KeyboardEvent) { setKeys(current => [ event.shiftKey && event.key !== 'Shift' diff --git a/packages/@headlessui-react/pages/_error.tsx b/packages/@headlessui-react/pages/_error.tsx index 3668256..38e9f7c 100644 --- a/packages/@headlessui-react/pages/_error.tsx +++ b/packages/@headlessui-react/pages/_error.tsx @@ -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 ( {children} @@ -25,7 +25,7 @@ export async function getStaticProps() { export default function Page(props: { examples: false | ExamplesType[] }) { if (props.examples === false) { - return + return } return ( diff --git a/packages/@headlessui-react/pages/listbox/listbox-with-pure-tailwind.tsx b/packages/@headlessui-react/pages/listbox/listbox-with-pure-tailwind.tsx index c4a089b..9d18fd2 100644 --- a/packages/@headlessui-react/pages/listbox/listbox-with-pure-tailwind.tsx +++ b/packages/@headlessui-react/pages/listbox/listbox-with-pure-tailwind.tsx @@ -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)]) }, []) diff --git a/packages/@headlessui-react/pages/listbox/multiple-elements.tsx b/packages/@headlessui-react/pages/listbox/multiple-elements.tsx index eaa0468..c84bc28 100644 --- a/packages/@headlessui-react/pages/listbox/multiple-elements.tsx +++ b/packages/@headlessui-react/pages/listbox/multiple-elements.tsx @@ -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)]) }, []) diff --git a/packages/@headlessui-react/pages/menu/menu-with-framer-motion.tsx b/packages/@headlessui-react/pages/menu/menu-with-framer-motion.tsx index 129efa2..9175aab 100644 --- a/packages/@headlessui-react/pages/menu/menu-with-framer-motion.tsx +++ b/packages/@headlessui-react/pages/menu/menu-with-framer-motion.tsx @@ -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 ( {children} diff --git a/packages/@headlessui-react/pages/menu/menu-with-popper.tsx b/packages/@headlessui-react/pages/menu/menu-with-popper.tsx index 551987f..e1f4eb0 100644 --- a/packages/@headlessui-react/pages/menu/menu-with-popper.tsx +++ b/packages/@headlessui-react/pages/menu/menu-with-popper.tsx @@ -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) } diff --git a/packages/@headlessui-react/pages/menu/menu-with-transition-and-popper.tsx b/packages/@headlessui-react/pages/menu/menu-with-transition-and-popper.tsx index d9ad495..2456ada 100644 --- a/packages/@headlessui-react/pages/menu/menu-with-transition-and-popper.tsx +++ b/packages/@headlessui-react/pages/menu/menu-with-transition-and-popper.tsx @@ -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] } }], diff --git a/packages/@headlessui-react/pages/menu/menu-with-transition.tsx b/packages/@headlessui-react/pages/menu/menu-with-transition.tsx index 7f2c53c..70ec606 100644 --- a/packages/@headlessui-react/pages/menu/menu-with-transition.tsx +++ b/packages/@headlessui-react/pages/menu/menu-with-transition.tsx @@ -1,4 +1,4 @@ -import * as React from 'react' +import React from 'react' import { Menu, Transition } from '@headlessui/react' function classNames(...classes) { diff --git a/packages/@headlessui-react/pages/menu/menu.tsx b/packages/@headlessui-react/pages/menu/menu.tsx index 5ebe19e..10339c6 100644 --- a/packages/@headlessui-react/pages/menu/menu.tsx +++ b/packages/@headlessui-react/pages/menu/menu.tsx @@ -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>) { +function CustomMenuItem(props: PropsOf) { return ( {({ active, disabled }) => ( diff --git a/packages/@headlessui-react/pages/menu/multiple-elements.tsx b/packages/@headlessui-react/pages/menu/multiple-elements.tsx index d27b96c..edb5686 100644 --- a/packages/@headlessui-react/pages/menu/multiple-elements.tsx +++ b/packages/@headlessui-react/pages/menu/multiple-elements.tsx @@ -1,4 +1,4 @@ -import * as React from 'react' +import React from 'react' import { Menu } from '@headlessui/react' function classNames(...classes) { diff --git a/packages/@headlessui-react/pages/switch/switch-with-pure-tailwind.tsx b/packages/@headlessui-react/pages/switch/switch-with-pure-tailwind.tsx index b0077af..cec516a 100644 --- a/packages/@headlessui-react/pages/switch/switch-with-pure-tailwind.tsx +++ b/packages/@headlessui-react/pages/switch/switch-with-pure-tailwind.tsx @@ -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 (
diff --git a/packages/@headlessui-react/pages/transitions/component-examples/dropdown.tsx b/packages/@headlessui-react/pages/transitions/component-examples/dropdown.tsx index f36d461..e391543 100644 --- a/packages/@headlessui-react/pages/transitions/component-examples/dropdown.tsx +++ b/packages/@headlessui-react/pages/transitions/component-examples/dropdown.tsx @@ -17,7 +17,7 @@ export default function Home() { } function Dropdown() { - const [isOpen, setIsOpen] = useState(false) + let [isOpen, setIsOpen] = useState(false) return (
diff --git a/packages/@headlessui-react/pages/transitions/component-examples/modal.tsx b/packages/@headlessui-react/pages/transitions/component-examples/modal.tsx index 31e4a00..cd03991 100644 --- a/packages/@headlessui-react/pages/transitions/component-examples/modal.tsx +++ b/packages/@headlessui-react/pages/transitions/component-examples/modal.tsx @@ -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}`]) diff --git a/packages/@headlessui-react/pages/transitions/component-examples/nested/hidden.tsx b/packages/@headlessui-react/pages/transitions/component-examples/nested/hidden.tsx index 7cec0cc..989f266 100644 --- a/packages/@headlessui-react/pages/transitions/component-examples/nested/hidden.tsx +++ b/packages/@headlessui-react/pages/transitions/component-examples/nested/hidden.tsx @@ -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 ( @@ -40,7 +40,7 @@ export default function Home() { ) } -function Box({ children }: { children?: React.ReactNode }) { +function Box({ children }: { children?: ReactNode }) { return ( diff --git a/packages/@headlessui-react/pages/transitions/full-page-examples/full-page-transition.tsx b/packages/@headlessui-react/pages/transitions/full-page-examples/full-page-transition.tsx index feab22c..33a96ea 100644 --- a/packages/@headlessui-react/pages/transitions/full-page-examples/full-page-transition.tsx +++ b/packages/@headlessui-react/pages/transitions/full-page-examples/full-page-transition.tsx @@ -21,7 +21,7 @@ export default function Shell() { } function usePrevious(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', diff --git a/packages/@headlessui-react/pages/transitions/full-page-examples/layout-with-sidebar.tsx b/packages/@headlessui-react/pages/transitions/full-page-examples/layout-with-sidebar.tsx index f6b3129..0cc33e0 100644 --- a/packages/@headlessui-react/pages/transitions/full-page-examples/layout-with-sidebar.tsx +++ b/packages/@headlessui-react/pages/transitions/full-page-examples/layout-with-sidebar.tsx @@ -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) { diff --git a/packages/@headlessui-react/playground-utils/hooks/use-popper.ts b/packages/@headlessui-react/playground-utils/hooks/use-popper.ts index ddd9445..4d10885 100644 --- a/packages/@headlessui-react/playground-utils/hooks/use-popper.ts +++ b/packages/@headlessui-react/playground-utils/hooks/use-popper.ts @@ -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 -): [React.RefCallback, React.RefCallback] { - const reference = React.useRef(null) - const popper = React.useRef(null) +): [RefCallback, RefCallback] { + let reference = useRef(null) + let popper = useRef(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 diff --git a/packages/@headlessui-react/playground-utils/resolve-all-examples.ts b/packages/@headlessui-react/playground-utils/resolve-all-examples.ts index 7f500a6..397c367 100644 --- a/packages/@headlessui-react/playground-utils/resolve-all-examples.ts +++ b/packages/@headlessui-react/playground-utils/resolve-all-examples.ts @@ -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 diff --git a/packages/@headlessui-react/src/components/listbox/listbox.test.tsx b/packages/@headlessui-react/src/components/listbox/listbox.test.tsx index a84bed5..953c81a 100644 --- a/packages/@headlessui-react/src/components/listbox/listbox.test.tsx +++ b/packages/@headlessui-react/src/components/listbox/listbox.test.tsx @@ -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 ', suppressConsoleLogs((name, Component) => { - expect(() => render(React.createElement(Component))).toThrowError( + expect(() => render(createElement(Component))).toThrowError( `<${name} /> is missing a parent 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( Trigger @@ -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 ( { 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 ( { 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', () => {
) - 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 ( { 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 ( { 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]) diff --git a/packages/@headlessui-react/src/components/listbox/listbox.tsx b/packages/@headlessui-react/src/components/listbox/listbox.tsx index 13a3d0e..eb1c3a2 100644 --- a/packages/@headlessui-react/src/components/listbox/listbox.tsx +++ b/packages/@headlessui-react/src/components/listbox/listbox.tsx @@ -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 - buttonRef: React.MutableRefObject - optionsRef: React.MutableRefObject + propsRef: MutableRefObject<{ value: unknown; onChange(value: unknown): void }> + labelRef: MutableRefObject + buttonRef: MutableRefObject + optionsRef: MutableRefObject 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 @@ -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] | null>(null) +let ListboxContext = createContext<[StateDefinition, Dispatch] | 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( props: Props & { 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( + let propsBag = useMemo( () => ({ 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( props: Props, - ref: React.Ref + ref: Ref ) { - 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) => { + let handleKeyDown = useCallback( + (event: ReactKeyboardEvent) => { 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( + let propsBag = useMemo( () => ({ 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( +function Label( props: Props ) { - 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( + let propsBag = useMemo( () => ({ 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 & PropsForFeatures, - ref: React.Ref + ref: Ref ) { - 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) => { + let handleKeyDown = useCallback( + (event: ReactKeyboardEvent) => { 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( + let propsBag = useMemo( () => ({ 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[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({ disabled, value }) + let bag = useRef({ 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, diff --git a/packages/@headlessui-react/src/components/menu/menu.test.tsx b/packages/@headlessui-react/src/components/menu/menu.test.tsx index 802ed5b..357d65b 100644 --- a/packages/@headlessui-react/src/components/menu/menu.test.tsx +++ b/packages/@headlessui-react/src/components/menu/menu.test.tsx @@ -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 ', suppressConsoleLogs((name, Component) => { - expect(() => render(React.createElement(Component))).toThrowError( + expect(() => render(createElement(Component))).toThrowError( `<${name} /> is missing a parent 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( Trigger @@ -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( @@ -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( Trigger @@ -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', () => {
) - 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( Trigger @@ -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( Trigger @@ -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( @@ -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]) diff --git a/packages/@headlessui-react/src/components/menu/menu.tsx b/packages/@headlessui-react/src/components/menu/menu.tsx index c07c7e3..68cf69d 100644 --- a/packages/@headlessui-react/src/components/menu/menu.tsx +++ b/packages/@headlessui-react/src/components/menu/menu.tsx @@ -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 - itemsRef: React.MutableRefObject + buttonRef: MutableRefObject + itemsRef: MutableRefObject 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 @@ -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] | null>(null) +let MenuContext = createContext<[StateDefinition, Dispatch] | 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( +export function Menu( props: Props ) { - 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( 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 ( @@ -177,8 +196,10 @@ export function Menu( // --- -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( props: Props, - ref: React.Ref + ref: Ref ) { - 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) => { + let handleKeyDown = useCallback( + (event: ReactKeyboardEvent) => { 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( props: Props & PropsForFeatures, - ref: React.Ref + ref: Ref ) { - 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) => { + let handleKeyDown = useCallback( + (event: ReactKeyboardEvent) => { 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( +function Item( props: Props & { disabled?: boolean onClick?: (event: { preventDefault: Function }) => void @@ -396,13 +418,12 @@ function Item( 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({ disabled }) + let bag = useRef({ disabled }) useIsoMorphicEffect(() => { bag.current.disabled = disabled @@ -417,8 +438,8 @@ function Item( 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( [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, diff --git a/packages/@headlessui-react/src/components/switch/switch.test.tsx b/packages/@headlessui-react/src/components/switch/switch.test.tsx index bd1b052..66742cd 100644 --- a/packages/@headlessui-react/src/components/switch/switch.test.tsx +++ b/packages/@headlessui-react/src/components/switch/switch.test.tsx @@ -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 ', suppressConsoleLogs((name, Component) => { - expect(() => render(React.createElement(Component))).toThrowError( + expect(() => render(createElement(Component))).toThrowError( `<${name} /> is missing a parent 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 ( { 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() // 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 ( { }) 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 ( (null) +let GroupContext = createContext(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 component.`) + let err = new Error(`<${component} /> is missing a parent 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(props: Props) { - const [switchElement, setSwitchElement] = React.useState(null) - const [labelElement, setLabelElement] = React.useState(null) +function Group(props: Props) { + let [switchElement, setSwitchElement] = useState(null) + let [labelElement, setLabelElement] = useState(null) - const context = React.useMemo( + let context = useMemo( () => ({ switch: switchElement, label: labelElement, @@ -45,6 +57,7 @@ function Group(props: }), [switchElement, setSwitchElement, labelElement, setLabelElement] ) + return ( {render(props, {}, DEFAULT_GROUP_TAG)} @@ -54,8 +67,10 @@ function Group(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( +export function Switch( props: Props< TTag, SwitchRenderPropArg, @@ -78,21 +93,21 @@ export function Switch 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) => { + let handleKeyUp = useCallback( + (event: ReactKeyboardEvent) => { if (event.key !== Keys.Tab) event.preventDefault() if (event.key === Keys.Space) toggle() }, @@ -100,13 +115,13 @@ export function Switch) => event.preventDefault(), + let handleKeyPress = useCallback( + (event: ReactKeyboardEvent) => event.preventDefault(), [] ) - const propsBag = React.useMemo(() => ({ checked }), [checked]) - const propsWeControl = { + let propsBag = useMemo(() => ({ checked }), [checked]) + let propsWeControl = { id, ref: groupContext === null ? undefined : groupContext.setSwitch, role: 'switch', @@ -128,23 +143,23 @@ export function Switch( +function Label( props: Props ) { - 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) } diff --git a/packages/@headlessui-react/src/components/transitions/transition.test.tsx b/packages/@headlessui-react/src/components/transitions/transition.test.tsx index 79a74a1..a6d20eb 100644 --- a/packages/@headlessui-react/src/components/transitions/transition.test.tsx +++ b/packages/@headlessui-react/src/components/transitions/transition.test.tsx @@ -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(Children) + let { container } = render(Children) expect(container.firstChild).toMatchInlineSnapshot(`
@@ -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( Children @@ -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( Children @@ -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( Children @@ -99,13 +99,13 @@ describe('Setup API', () => { }) it('should render nothing when the show prop is false', () => { - const { container } = render(Children) + let { container } = render(Children) expect(container.firstChild).toMatchInlineSnapshot(`null`) }) it('should be possible to change the underlying DOM tag', () => { - const { container } = render( + let { container } = render( Children @@ -119,8 +119,8 @@ describe('Setup API', () => { }) it('should be possible to use a render prop', () => { - const { container } = render( - + let { container } = render( + {() => Children} ) @@ -143,7 +143,7 @@ describe('Setup API', () => { expect(() => { render( - + {() => } ) @@ -182,7 +182,7 @@ describe('Setup API', () => { }) it('should be possible to nest transition components', () => { - const { container } = render( + let { container } = render(
Sidebar @@ -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(
Sidebar @@ -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(
Sidebar @@ -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(
- {() => } - - {() =>
Content
} -
+ {() => } + {() =>
Content
}
) @@ -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(
- + {() => (
- - {() => } - - + {() => } + {() =>
Content
}
@@ -334,12 +330,8 @@ describe('Setup API', () => { render(
- - {() => Sidebar} - - - {() => Content} - + {() => Sidebar} + {() => Content}
) @@ -361,7 +353,7 @@ describe('Setup API', () => { expect(() => { render(
- + {() => ( {() => } @@ -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( { }) 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( { 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(, [ + let timeline = await executeTimeline(, [ // 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(, [ + let timeline = await executeTimeline(, [ // 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(, [ + let timeline = await executeTimeline(, [ // 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(, [ + let timeline = await executeTimeline(, [ // 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(, [ + let timeline = await executeTimeline(, [ // 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(, [ + let timeline = await executeTimeline(, [ // 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(, [ + let timeline = await executeTimeline(, [ // 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(, [ + let timeline = await executeTimeline(, [ // 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(, [ + let timeline = await executeTimeline(, [ // 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(, [ + let timeline = await executeTimeline(, [ // 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(, [ + let timeline = await executeTimeline(, [ // 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) }) diff --git a/packages/@headlessui-react/src/components/transitions/transition.tsx b/packages/@headlessui-react/src/components/transitions/transition.tsx index 11c959c..dff7a40 100644 --- a/packages/@headlessui-react/src/components/transitions/transition.tsx +++ b/packages/@headlessui-react/src/components/transitions/transition.tsx @@ -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 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(null) +} +let TransitionContext = createContext(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 = Props & PropsForFeatures & - 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 is used but it is missing a parent .') @@ -61,7 +75,7 @@ function useTransitionContext() { } function useParentNesting() { - const context = React.useContext(NestingContext) + let context = useContext(NestingContext) if (context === null) { throw new Error('A is used but it is missing a parent .') @@ -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(null) +let NestingContext = createContext(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([]) - const mounted = useIsMounted() + let doneRef = useRef(done) + let transitionableChildren = useRef([]) + 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 -const TransitionChildRenderFeatures = Features.RenderStrategy +let DEFAULT_TRANSITION_CHILD_TAG = 'div' as const +type TransitionChildRenderPropArg = MutableRefObject +let TransitionChildRenderFeatures = Features.RenderStrategy -function TransitionChild( +function TransitionChild( props: TransitionChildProps ) { - const { + let { // Event "handlers" beforeEnter, afterEnter, @@ -188,20 +202,20 @@ function TransitionChild(null) - const [state, setState] = React.useState(TreeStates.Visible) - const strategy = rest.unmount ? RenderStrategy.Unmount : RenderStrategy.Hidden + } = props as Expand + let container = useRef(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 { + 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 @@ -314,28 +328,28 @@ function TransitionChild( +export function Transition( props: TransitionChildProps & { show: boolean; appear?: boolean } ) { - const { show, appear = false, unmount, ...passthroughProps } = props + let { show, appear = false, unmount, ...passthroughProps } = props as Expand if (![true, false].includes(show)) { throw new Error('A 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( + let initial = useIsInitialRender() + let transitionBag = useMemo( () => ({ 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 @@ -352,11 +366,11 @@ export function Transition, }, propsBag, - React.Fragment, + Fragment, TransitionChildRenderFeatures, state === TreeStates.Visible )} diff --git a/packages/@headlessui-react/src/components/transitions/utils/transition.test.ts b/packages/@headlessui-react/src/components/transitions/utils/transition.test.ts index 31e738b..0b0f51f 100644 --- a/packages/@headlessui-react/src/components/transitions/utils/transition.test.ts +++ b/packages/@headlessui-react/src/components/transitions/utils/transition.test.ts @@ -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('
') - 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 () => `
` ) - 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) }) diff --git a/packages/@headlessui-react/src/components/transitions/utils/transition.ts b/packages/@headlessui-react/src/components/transitions/utils/transition.ts index 25d1915..b74df00 100644 --- a/packages/@headlessui-react/src/components/transitions/utils/transition.ts +++ b/packages/@headlessui-react/src/components/transitions/utils/transition.ts @@ -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) diff --git a/packages/@headlessui-react/src/hooks/__mocks__/use-id.ts b/packages/@headlessui-react/src/hooks/__mocks__/use-id.ts index aa77080..7a69143 100644 --- a/packages/@headlessui-react/src/hooks/__mocks__/use-id.ts +++ b/packages/@headlessui-react/src/hooks/__mocks__/use-id.ts @@ -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 } diff --git a/packages/@headlessui-react/src/hooks/use-computed.ts b/packages/@headlessui-react/src/hooks/use-computed.ts index 991b2c9..980ef9b 100644 --- a/packages/@headlessui-react/src/hooks/use-computed.ts +++ b/packages/@headlessui-react/src/hooks/use-computed.ts @@ -1,9 +1,9 @@ -import * as React from 'react' +import { useState, useRef } from 'react' import { useIsoMorphicEffect } from './use-iso-morphic-effect' export function useComputed(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]) diff --git a/packages/@headlessui-react/src/hooks/use-disposables.ts b/packages/@headlessui-react/src/hooks/use-disposables.ts index 35467d9..b4e265a 100644 --- a/packages/@headlessui-react/src/hooks/use-disposables.ts +++ b/packages/@headlessui-react/src/hooks/use-disposables.ts @@ -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 } diff --git a/packages/@headlessui-react/src/hooks/use-id.ts b/packages/@headlessui-react/src/hooks/use-id.ts index e4d2333..d805022 100644 --- a/packages/@headlessui-react/src/hooks/use-id.ts +++ b/packages/@headlessui-react/src/hooks/use-id.ts @@ -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 }, []) diff --git a/packages/@headlessui-react/src/hooks/use-is-initial-render.ts b/packages/@headlessui-react/src/hooks/use-is-initial-render.ts index 875fd6f..a61ca7f 100644 --- a/packages/@headlessui-react/src/hooks/use-is-initial-render.ts +++ b/packages/@headlessui-react/src/hooks/use-is-initial-render.ts @@ -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 }, []) diff --git a/packages/@headlessui-react/src/hooks/use-is-mounted.ts b/packages/@headlessui-react/src/hooks/use-is-mounted.ts index 1803be6..66d6c0a 100644 --- a/packages/@headlessui-react/src/hooks/use-is-mounted.ts +++ b/packages/@headlessui-react/src/hooks/use-is-mounted.ts @@ -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 } diff --git a/packages/@headlessui-react/src/hooks/use-iso-morphic-effect.ts b/packages/@headlessui-react/src/hooks/use-iso-morphic-effect.ts index 61a5753..c2bf43f 100644 --- a/packages/@headlessui-react/src/hooks/use-iso-morphic-effect.ts +++ b/packages/@headlessui-react/src/hooks/use-iso-morphic-effect.ts @@ -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 diff --git a/packages/@headlessui-react/src/hooks/use-sync-refs.ts b/packages/@headlessui-react/src/hooks/use-sync-refs.ts index b2a5dcd..a90a4b7 100644 --- a/packages/@headlessui-react/src/hooks/use-sync-refs.ts +++ b/packages/@headlessui-react/src/hooks/use-sync-refs.ts @@ -1,9 +1,9 @@ -import * as React from 'react' +import { useCallback } from 'react' export function useSyncRefs( ...refs: (React.MutableRefObject | ((instance: TType) => void) | null)[] ) { - return React.useCallback( + return useCallback( (value: TType) => { refs.forEach(ref => { if (ref === null) return diff --git a/packages/@headlessui-react/src/index.test.ts b/packages/@headlessui-react/src/index.test.ts index 15887eb..e5fb10e 100644 --- a/packages/@headlessui-react/src/index.test.ts +++ b/packages/@headlessui-react/src/index.test.ts @@ -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']) }) diff --git a/packages/@headlessui-react/src/test-utils/accessibility-assertions.ts b/packages/@headlessui-react/src/test-utils/accessibility-assertions.ts index d70f60f..7957ff2 100644 --- a/packages/@headlessui-react/src/test-utils/accessibility-assertions.ts +++ b/packages/@headlessui-react/src/test-utils/accessibility-assertions.ts @@ -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 } diff --git a/packages/@headlessui-react/src/test-utils/execute-timeline.ts b/packages/@headlessui-react/src/test-utils/execute-timeline.ts index 10289d3..0275fc6 100644 --- a/packages/@headlessui-react/src/test-utils/execute-timeline.ts +++ b/packages/@headlessui-react/src/test-utils/execute-timeline.ts @@ -8,11 +8,11 @@ export async function executeTimeline( element: JSX.Element, steps: ((tools: ReturnType) => (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 } diff --git a/packages/@headlessui-react/src/test-utils/interactions.ts b/packages/@headlessui-react/src/test-utils/interactions.ts index 283162b..221c9cb 100644 --- a/packages/@headlessui-react/src/test-utils/interactions.ts +++ b/packages/@headlessui-react/src/test-utils/interactions.ts @@ -8,7 +8,7 @@ function nextFrame(cb: Function): void { ) } -export const Keys: Record> = { +export let Keys: Record> = { 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) { - 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) { // Credit: // - https://stackoverflow.com/a/30753870 -const focusableSelector = [ +let focusableSelector = [ '[contentEditable=true]', '[tabindex]', 'a[href]', diff --git a/packages/@headlessui-react/src/test-utils/report-dom-node-changes.ts b/packages/@headlessui-react/src/test-utils/report-dom-node-changes.ts index dcf4434..2f33091 100644 --- a/packages/@headlessui-react/src/test-utils/report-dom-node-changes.ts +++ b/packages/@headlessui-react/src/test-utils/report-dom-node-changes.ts @@ -1,12 +1,12 @@ import { disposables } from '../utils/disposables' export function reportChanges(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) diff --git a/packages/@headlessui-react/src/test-utils/suppress-console-logs.ts b/packages/@headlessui-react/src/test-utils/suppress-console-logs.ts index 07f7702..da8e6d1 100644 --- a/packages/@headlessui-react/src/test-utils/suppress-console-logs.ts +++ b/packages/@headlessui-react/src/test-utils/suppress-console-logs.ts @@ -8,7 +8,7 @@ export function suppressConsoleLogs( type: FunctionPropertyNames = '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((resolve, reject) => { Promise.resolve(cb(...args)).then(resolve, reject) diff --git a/packages/@headlessui-react/src/types.ts b/packages/@headlessui-react/src/types.ts index bbb7ba2..c968d5c 100644 --- a/packages/@headlessui-react/src/types.ts +++ b/packages/@headlessui-react/src/types.ts @@ -3,6 +3,8 @@ const __: unique symbol = Symbol('__placeholder__') export type __ = typeof __ +export type Expand = T extends infer O ? { [K in keyof O]: O[K] } : never + export type PropsOf = TTag extends React.ElementType ? React.ComponentProps : never diff --git a/packages/@headlessui-react/src/utils/calculate-active-index.ts b/packages/@headlessui-react/src/utils/calculate-active-index.ts index 31b9e8c..cc296a9 100644 --- a/packages/@headlessui-react/src/utils/calculate-active-index.ts +++ b/packages/@headlessui-react/src/utils/calculate-active-index.ts @@ -31,19 +31,19 @@ export function calculateActiveIndex( 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( }) case Focus.Last: { - const idx = items + let idx = items .slice() .reverse() .findIndex(item => !resolvers.resolveDisabled(item)) diff --git a/packages/@headlessui-react/src/utils/disposables.ts b/packages/@headlessui-react/src/utils/disposables.ts index d782a5c..7c9a388 100644 --- a/packages/@headlessui-react/src/utils/disposables.ts +++ b/packages/@headlessui-react/src/utils/disposables.ts @@ -1,9 +1,9 @@ export function disposables() { - const disposables: Function[] = [] + let disposables: Function[] = [] - const api = { + let api = { requestAnimationFrame(...args: Parameters) { - const raf = requestAnimationFrame(...args) + let raf = requestAnimationFrame(...args) api.add(() => cancelAnimationFrame(raf)) }, @@ -14,7 +14,7 @@ export function disposables() { }, setTimeout(...args: Parameters) { - 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() + } }, } diff --git a/packages/@headlessui-react/src/utils/match.ts b/packages/@headlessui-react/src/utils/match.ts index 939bd42..80496d1 100644 --- a/packages/@headlessui-react/src/utils/match.ts +++ b/packages/@headlessui-react/src/utils/match.ts @@ -4,11 +4,11 @@ export function match(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) } diff --git a/packages/@headlessui-react/src/utils/render.test.tsx b/packages/@headlessui-react/src/utils/render.test.tsx index 3bb895c..1af1e78 100644 --- a/packages/@headlessui-react/src/utils/render.test.tsx +++ b/packages/@headlessui-react/src/utils/render.test.tsx @@ -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( + let bag = {} + function Dummy( props: Props & Partial<{ a: any; b: any; c: any }> ) { return
{render(props, bag, '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({ + function MyComponent({ innerRef, ...props - }: Props & { innerRef: React.Ref }) { + }: Props & { innerRef: Ref }) { return
} - function OtherDummy(props: Props) { + function OtherDummy(props: Props) { return
{render({ ...props, ref }, bag, '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(Contents) + it('should be possible to render the children only when the `as` prop is set to Fragment', () => { + testRender(Contents) expect(contents()).toMatchInlineSnapshot(` "
{ `) }) - 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( - + {() => Contents} ) @@ -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 - + // @ts-expect-error className cannot be applied to a Fragment + Contents A Contents B @@ -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( - + Contents A Contents B @@ -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 - + // @ts-expect-error className cannot be applied to a Fragment + Contents ) @@ -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( + let bag = {} + let EnabledFeatures = Features.Static + function Dummy( props: Props & { show: boolean } & PropsForFeatures ) { - const { show, ...rest } = props + let { show, ...rest } = props return
{render(rest, bag, 'div', EnabledFeatures, show)}
} @@ -364,12 +364,12 @@ function testRenderStrategyFeature(Dummy) { } describe('Features.RenderStrategy', () => { - const bag = {} - const EnabledFeatures = Features.RenderStrategy - function Dummy( + let bag = {} + let EnabledFeatures = Features.RenderStrategy + function Dummy( props: Props & { show: boolean } & PropsForFeatures ) { - const { show, ...rest } = props + let { show, ...rest } = props return
{render(rest, bag, 'div', EnabledFeatures, show)}
} @@ -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( + let bag = {} + let EnabledFeatures = Features.Static | Features.RenderStrategy + function Dummy( props: Props & { show: boolean } & PropsForFeatures ) { - const { show, ...rest } = props + let { show, ...rest } = props return
{render(rest, bag, 'div', EnabledFeatures, show)}
} diff --git a/packages/@headlessui-react/src/utils/render.ts b/packages/@headlessui-react/src/utils/render.ts index a255c0d..b001f6e 100644 --- a/packages/@headlessui-react/src/utils/render.ts +++ b/packages/@headlessui-react/src/utils/render.ts @@ -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 = XOR< PropsForFeature > -export function render( - props: Props & PropsForFeatures, +export function render( + props: Expand & PropsForFeatures>, 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 + let { static: isStatic = false, ...rest } = props as PropsForFeatures // When the `static` prop is passed as `true`, then the user is in control, thus we don't care about anything else if (isStatic) return _render(rest, propsBag, defaultTag) } if (featureFlags & Features.RenderStrategy) { - const { unmount = true, ...rest } = props as PropsForFeatures - const strategy = unmount ? RenderStrategy.Unmount : RenderStrategy.Hidden + let { unmount = true, ...rest } = props as PropsForFeatures + let strategy = unmount ? RenderStrategy.Unmount : RenderStrategy.Hidden return match(strategy, { [RenderStrategy.Unmount]() { @@ -77,40 +87,40 @@ export function render( - props: Props & { ref?: unknown }, +function _render( + props: Expand & { 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 `` - 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( } } - 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(component: T): T { - return React.forwardRef((component as unknown) as any) as any + return forwardRef((component as unknown) as any) as any } function compact>(object: T) { diff --git a/packages/@headlessui-vue/examples/src/App.vue b/packages/@headlessui-vue/examples/src/App.vue index 9eec170..4230baa 100644 --- a/packages/@headlessui-vue/examples/src/App.vue +++ b/packages/@headlessui-vue/examples/src/App.vue @@ -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 } }, diff --git a/packages/@headlessui-vue/examples/src/KeyCaster.vue b/packages/@headlessui-vue/examples/src/KeyCaster.vue index 1f23b04..8a245e3 100644 --- a/packages/@headlessui-vue/examples/src/KeyCaster.vue +++ b/packages/@headlessui-vue/examples/src/KeyCaster.vue @@ -15,9 +15,9 @@