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