feat: add render features + render strategy (#106)
* add unmount strategy to README (React)
* add unmount strategy to README (Vue)
* add different render features (React)
* use render features in Menu and Listbox (React)
* add different render features (Vue)
* use render features in Menu and Listbox (Vue)
* bump dependencies
* add ability to change the ref property using `refName`
Example use case:
```tsx
// Some components have this API with an `innerRef`. The suggested approach is to use
// `React.forwardRef` so that you get the actual `ref` value. However if you already have this
// `innerRef` API than we can use the `refName="innerRef"` to give the `ref` prop a good name. It
// defaults to `ref` so that it still works everywhere else.
function MyButton({ innerRef, ...props }) {
return <button ref={innerRef} {...props} />
}
<Menu.Button as={MyButton} refName="innerRef" />
```
* small cleanup, move refs to props we control
* add tests for the render abstraction (Render)
+ use the unique __ symbol as a default value in the Props type for the
omitable props.
* use render features in Transition (React)
* add/update Transition examples to also showcase the `unmount={false}` render strategy
* bump dependencies
* add example with nested unmount/hide transitions
* add unmount to Transition documentation
This commit is contained in:
+3
-3
@@ -34,11 +34,11 @@
|
||||
"devDependencies": {
|
||||
"@tailwindcss/ui": "^0.6.2",
|
||||
"@testing-library/jest-dom": "^5.11.4",
|
||||
"@types/node": "^14.11.8",
|
||||
"@types/node": "^14.11.10",
|
||||
"husky": "^4.3.0",
|
||||
"lint-staged": "^10.4.0",
|
||||
"lint-staged": "^10.4.2",
|
||||
"prismjs": "^1.22.0",
|
||||
"tailwindcss": "^1.9.1",
|
||||
"tailwindcss": "^1.9.4",
|
||||
"tsdx": "^0.14.1",
|
||||
"tslib": "^2.0.3",
|
||||
"typescript": "^3.9.7"
|
||||
|
||||
@@ -317,6 +317,7 @@ function MyComponent({ isShowing }) {
|
||||
| `show` | Boolean | Whether the children should be shown or hidden. |
|
||||
| `as` | String Component _(Default: `'div'`)_ | The element or component to render in place of the `Transition` itself. |
|
||||
| `appear` | Boolean _(Default: `false`)_ | Whether the transition should run on initial mount. |
|
||||
| `unmount` | Boolean _(Default: `true`)_ | Whether the element should be unmounted or hidden based on the show state. |
|
||||
| `enter` | String _(Default: '')_ | Classes to add to the transitioning element during the entire enter phase. |
|
||||
| `enterFrom` | String _(Default: '')_ | Classes to add to the transitioning element before the enter phase starts. |
|
||||
| `enterTo` | String _(Default: '')_ | Classes to add to the transitioning element immediately after the enter phase starts. |
|
||||
@@ -358,6 +359,7 @@ function MyComponent({ isShowing }) {
|
||||
| ----------- | ------------------------------------- | ------------------------------------------------------------------------------------- |
|
||||
| `as` | String Component _(Default: `'div'`)_ | The element or component to render in place of the `Transition.Child` itself. |
|
||||
| `appear` | Boolean _(Default: `false`)_ | Whether the transition should run on initial mount. |
|
||||
| `unmount` | Boolean _(Default: `true`)_ | Whether the element should be unmounted or hidden based on the show state. |
|
||||
| `enter` | String _(Default: '')_ | Classes to add to the transitioning element during the entire enter phase. |
|
||||
| `enterFrom` | String _(Default: '')_ | Classes to add to the transitioning element before the enter phase starts. |
|
||||
| `enterTo` | String _(Default: '')_ | Classes to add to the transitioning element immediately after the enter phase starts. |
|
||||
@@ -726,10 +728,13 @@ function MyDropdown() {
|
||||
|
||||
##### Props
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| -------- | ------------------- | ------- | --------------------------------------------------------------------------- |
|
||||
| `as` | String \| Component | `div` | The element or component the `Menu.Items` should render as. |
|
||||
| `static` | Boolean | `false` | Whether the element should ignore the internally managed open/closed state. |
|
||||
| Prop | Type | Default | Description |
|
||||
| --------- | ------------------- | ------- | --------------------------------------------------------------------------------- |
|
||||
| `as` | String \| Component | `div` | The element or component the `Menu.Items` should render as. |
|
||||
| `static` | Boolean | `false` | Whether the element should ignore the internally managed open/closed state. |
|
||||
| `unmount` | Boolean | `true` | Whether the element should be unmounted or hidden based on the open/closed state. |
|
||||
|
||||
> **note**: `static` and `unmount` can not be used at the same time. You will get a TypeScript error if you try to do it.
|
||||
|
||||
##### Render prop object
|
||||
|
||||
@@ -1231,10 +1236,13 @@ function MyListbox() {
|
||||
|
||||
##### Props
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| -------- | ------------------- | ------- | --------------------------------------------------------------------------- |
|
||||
| `as` | String \| Component | `ul` | The element or component the `Listbox.Options` should render as. |
|
||||
| `static` | Boolean | `false` | Whether the element should ignore the internally managed open/closed state. |
|
||||
| Prop | Type | Default | Description |
|
||||
| --------- | ------------------- | ------- | --------------------------------------------------------------------------------- |
|
||||
| `as` | String \| Component | `ul` | The element or component the `Listbox.Options` should render as. |
|
||||
| `static` | Boolean | `false` | Whether the element should ignore the internally managed open/closed state. |
|
||||
| `unmount` | Boolean | `true` | Whether the element should be unmounted or hidden based on the open/closed state. |
|
||||
|
||||
> **note**: `static` and `unmount` can not be used at the same time. You will get a TypeScript error if you try to do it.
|
||||
|
||||
##### Render prop object
|
||||
|
||||
|
||||
@@ -31,14 +31,14 @@
|
||||
"react": ">=16"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^16.9.52",
|
||||
"@types/react": "^16.9.53",
|
||||
"@types/react-dom": "^16.9.8",
|
||||
"@popperjs/core": "^2.5.3",
|
||||
"@testing-library/react": "^11.0.4",
|
||||
"@testing-library/react": "^11.1.0",
|
||||
"framer-motion": "^2.9.1",
|
||||
"next": "9.5.5",
|
||||
"react": "^16.13.1",
|
||||
"react-dom": "^16.13.1",
|
||||
"react": "^16.14.0",
|
||||
"react-dom": "^16.14.0",
|
||||
"snapshot-diff": "^0.8.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
import React, { useState } from 'react'
|
||||
import { Transition } from '@headlessui/react'
|
||||
|
||||
export default function Home() {
|
||||
const [isOpen, setIsOpen] = useState(true)
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex justify-center w-screen h-full p-12 bg-gray-50">
|
||||
<div className="space-y-2 w-96">
|
||||
<span className="inline-flex rounded-md shadow-sm">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setIsOpen(v => !v)}
|
||||
className="inline-flex items-center px-3 py-2 text-sm font-medium leading-4 text-gray-700 transition bg-white border border-gray-300 rounded-md duration-150-out hover:text-gray-500 focus:outline-none focus:border-blue-300 focus:shadow-outline-blue active:text-gray-800 active:bg-gray-50"
|
||||
>
|
||||
{isOpen ? 'Hide' : 'Show'}
|
||||
</button>
|
||||
</span>
|
||||
|
||||
<Transition show={isOpen} unmount={false}>
|
||||
<Box>
|
||||
<Box>
|
||||
<Box>
|
||||
<Box />
|
||||
</Box>
|
||||
<Box>
|
||||
<Box>
|
||||
<Box>
|
||||
<Box />
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</Transition>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function Box({ children }: { children?: React.ReactNode }) {
|
||||
return (
|
||||
<Transition.Child
|
||||
unmount={false}
|
||||
enter="transition translate duration-300"
|
||||
enterFrom="transform -translate-x-full"
|
||||
enterTo="transform translate-x-0"
|
||||
leave="transition translate duration-300"
|
||||
leaveFrom="transform translate-x-0"
|
||||
leaveTo="transform translate-x-full"
|
||||
>
|
||||
<div className="p-4 space-y-2 text-sm font-semibold tracking-wide text-gray-700 uppercase bg-white rounded-md shadow">
|
||||
<span>This is a box</span>
|
||||
{children}
|
||||
</div>
|
||||
</Transition.Child>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
import React, { useState } from 'react'
|
||||
import { Transition } from '@headlessui/react'
|
||||
|
||||
export default function Home() {
|
||||
const [isOpen, setIsOpen] = useState(true)
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex justify-center w-screen h-full p-12 bg-gray-50">
|
||||
<div className="space-y-2 w-96">
|
||||
<span className="inline-flex rounded-md shadow-sm">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setIsOpen(v => !v)}
|
||||
className="inline-flex items-center px-3 py-2 text-sm font-medium leading-4 text-gray-700 transition bg-white border border-gray-300 rounded-md duration-150-out hover:text-gray-500 focus:outline-none focus:border-blue-300 focus:shadow-outline-blue active:text-gray-800 active:bg-gray-50"
|
||||
>
|
||||
{isOpen ? 'Hide' : 'Show'}
|
||||
</button>
|
||||
</span>
|
||||
|
||||
<Transition show={isOpen} unmount={true}>
|
||||
<Box>
|
||||
<Box>
|
||||
<Box>
|
||||
<Box />
|
||||
</Box>
|
||||
<Box>
|
||||
<Box>
|
||||
<Box>
|
||||
<Box />
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</Transition>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function Box({ children }: { children?: React.ReactNode }) {
|
||||
return (
|
||||
<Transition.Child
|
||||
unmount={true}
|
||||
enter="transition translate duration-300"
|
||||
enterFrom="transform -translate-x-full"
|
||||
enterTo="transform translate-x-0"
|
||||
leave="transition translate duration-300"
|
||||
leaveFrom="transform translate-x-0"
|
||||
leaveTo="transform translate-x-full"
|
||||
>
|
||||
<div className="p-4 space-y-2 text-sm font-semibold tracking-wide text-gray-700 uppercase bg-white rounded-md shadow">
|
||||
<span>This is a box</span>
|
||||
{children}
|
||||
</div>
|
||||
</Transition.Child>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import React, { useState } from 'react'
|
||||
import { Transition } from '@headlessui/react'
|
||||
|
||||
export default function Home() {
|
||||
const [isOpen, setIsOpen] = useState(true)
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex justify-center w-screen h-full p-12 bg-gray-50">
|
||||
<div className="space-y-2 w-96">
|
||||
<span className="inline-flex rounded-md shadow-sm">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setIsOpen(v => !v)}
|
||||
className="inline-flex items-center px-3 py-2 text-sm font-medium leading-4 text-gray-700 transition duration-150 ease-in-out bg-white border border-gray-300 rounded-md hover:text-gray-500 focus:outline-none focus:border-blue-300 focus:shadow-outline-blue active:text-gray-800 active:bg-gray-50"
|
||||
>
|
||||
{isOpen ? 'Hide' : 'Show'}
|
||||
</button>
|
||||
</span>
|
||||
|
||||
<Transition
|
||||
show={isOpen}
|
||||
unmount={false}
|
||||
enter="transition ease-out duration-300"
|
||||
enterFrom="transform opacity-0"
|
||||
enterTo="transform opacity-100"
|
||||
leave="transition ease-in duration-300"
|
||||
leaveFrom="transform opacity-100"
|
||||
leaveTo="transform opacity-0"
|
||||
className="p-4 bg-white rounded-md shadow"
|
||||
>
|
||||
Contents to show and hide
|
||||
</Transition>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
+6
-3
@@ -26,9 +26,10 @@ export default function App() {
|
||||
|
||||
<div className="flex h-screen overflow-hidden bg-cool-gray-100">
|
||||
{/* Off-canvas menu for mobile */}
|
||||
<Transition show={mobileOpen} className="fixed inset-0 z-40 flex">
|
||||
<Transition show={mobileOpen} unmount={false} className="fixed inset-0 z-40 flex">
|
||||
{/* Off-canvas menu overlay, show/hide based on off-canvas menu state. */}
|
||||
<Transition.Child
|
||||
unmount={false}
|
||||
enter="transition-opacity ease-linear duration-300"
|
||||
enterFrom="opacity-0"
|
||||
enterTo="opacity-100"
|
||||
@@ -36,8 +37,8 @@ export default function App() {
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
{ref => (
|
||||
<div ref={ref} className="fixed inset-0">
|
||||
{() => (
|
||||
<div className="fixed inset-0">
|
||||
<div
|
||||
onClick={() => setMobileOpen(false)}
|
||||
className="absolute inset-0 opacity-75 bg-cool-gray-600"
|
||||
@@ -48,6 +49,7 @@ export default function App() {
|
||||
|
||||
{/* Off-canvas menu, show/hide based on off-canvas menu state. */}
|
||||
<Transition.Child
|
||||
unmount={false}
|
||||
enter="transition ease-in-out duration-300 transform"
|
||||
enterFrom="-translate-x-full"
|
||||
enterTo="translate-x-0"
|
||||
@@ -58,6 +60,7 @@ export default function App() {
|
||||
>
|
||||
<div className="absolute top-0 right-0 p-1 -mr-14">
|
||||
<Transition.Child
|
||||
unmount={false}
|
||||
className="flex items-center justify-center w-12 h-12 rounded-full focus:outline-none focus:bg-cool-gray-600"
|
||||
aria-label="Close sidebar"
|
||||
as="button"
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -6,7 +6,7 @@ import { useIsoMorphicEffect } from '../../hooks/use-iso-morphic-effect'
|
||||
import { useComputed } from '../../hooks/use-computed'
|
||||
import { useSyncRefs } from '../../hooks/use-sync-refs'
|
||||
import { Props } from '../../types'
|
||||
import { forwardRefWithAs, render } from '../../utils/render'
|
||||
import { Features, forwardRefWithAs, PropsForFeatures, render } from '../../utils/render'
|
||||
import { match } from '../../utils/match'
|
||||
import { disposables } from '../../utils/disposables'
|
||||
import { Keys } from '../keyboard'
|
||||
@@ -114,7 +114,11 @@ const reducers: {
|
||||
action: Extract<Actions, { type: P }>
|
||||
) => StateDefinition
|
||||
} = {
|
||||
[ActionTypes.CloseListbox]: state => ({ ...state, listboxState: ListboxStates.Closed }),
|
||||
[ActionTypes.CloseListbox]: state => ({
|
||||
...state,
|
||||
activeOptionIndex: null,
|
||||
listboxState: ListboxStates.Closed,
|
||||
}),
|
||||
[ActionTypes.OpenListbox]: state => ({ ...state, listboxState: ListboxStates.Open }),
|
||||
[ActionTypes.GoToOption]: (state, action) => {
|
||||
const activeOptionIndex = calculateActiveOptionIndex(state, action.focus, action.id)
|
||||
@@ -375,11 +379,7 @@ function Label<TTag extends React.ElementType = typeof DEFAULT_LABEL_TAG>(
|
||||
() => ({ open: state.listboxState === ListboxStates.Open }),
|
||||
[state]
|
||||
)
|
||||
const propsWeControl = {
|
||||
ref: state.labelRef,
|
||||
id,
|
||||
onPointerUp: handlePointerUp,
|
||||
}
|
||||
const propsWeControl = { ref: state.labelRef, id, onPointerUp: handlePointerUp }
|
||||
return render({ ...props, ...propsWeControl }, propsBag, DEFAULT_LABEL_TAG)
|
||||
}
|
||||
|
||||
@@ -397,24 +397,15 @@ type OptionsPropsWeControl =
|
||||
const DEFAULT_OPTIONS_TAG = 'ul'
|
||||
|
||||
type OptionsRenderPropArg = { open: boolean }
|
||||
|
||||
type ListboxOptionsProp<TTag> = Props<TTag, OptionsRenderPropArg, OptionsPropsWeControl> & {
|
||||
static?: boolean
|
||||
}
|
||||
const OptionsRenderFeatures = Features.RenderStrategy | Features.Static
|
||||
|
||||
const Options = forwardRefWithAs(function Options<
|
||||
TTag extends React.ElementType = typeof DEFAULT_OPTIONS_TAG
|
||||
>(props: ListboxOptionsProp<TTag>, ref: React.Ref<HTMLUListElement>) {
|
||||
const {
|
||||
enter,
|
||||
enterFrom,
|
||||
enterTo,
|
||||
leave,
|
||||
leaveFrom,
|
||||
leaveTo,
|
||||
static: isStatic = false,
|
||||
...passthroughProps
|
||||
} = props
|
||||
>(
|
||||
props: Props<TTag, OptionsRenderPropArg, OptionsPropsWeControl> &
|
||||
PropsForFeatures<typeof OptionsRenderFeatures>,
|
||||
ref: React.Ref<HTMLUListElement>
|
||||
) {
|
||||
const [state, dispatch] = useListboxContext([Listbox.name, Options.name].join('.'))
|
||||
const optionsRef = useSyncRefs(state.optionsRef, ref)
|
||||
|
||||
@@ -500,14 +491,16 @@ const Options = forwardRefWithAs(function Options<
|
||||
onKeyDown: handleKeyDown,
|
||||
role: 'listbox',
|
||||
tabIndex: 0,
|
||||
ref: optionsRef,
|
||||
}
|
||||
|
||||
if (!isStatic && state.listboxState === ListboxStates.Closed) return null
|
||||
const passthroughProps = props
|
||||
|
||||
return render(
|
||||
{ ...passthroughProps, ...propsWeControl, ...{ ref: optionsRef } },
|
||||
{ ...passthroughProps, ...propsWeControl },
|
||||
propsBag,
|
||||
DEFAULT_OPTIONS_TAG
|
||||
DEFAULT_OPTIONS_TAG,
|
||||
OptionsRenderFeatures,
|
||||
state.listboxState === ListboxStates.Open
|
||||
)
|
||||
})
|
||||
|
||||
@@ -570,17 +563,19 @@ function Option<
|
||||
}, [bag, id])
|
||||
|
||||
useIsoMorphicEffect(() => {
|
||||
if (state.listboxState !== ListboxStates.Open) return
|
||||
if (!selected) return
|
||||
dispatch({ type: ActionTypes.GoToOption, focus: Focus.Specific, id })
|
||||
document.getElementById(id)?.focus?.()
|
||||
}, [])
|
||||
}, [state.listboxState])
|
||||
|
||||
useIsoMorphicEffect(() => {
|
||||
if (state.listboxState !== ListboxStates.Open) return
|
||||
if (!active) return
|
||||
const d = disposables()
|
||||
d.nextFrame(() => document.getElementById(id)?.scrollIntoView?.({ block: 'nearest' }))
|
||||
return d.dispose
|
||||
}, [active])
|
||||
}, [active, state.listboxState])
|
||||
|
||||
const handleClick = React.useCallback(
|
||||
(event: { preventDefault: Function }) => {
|
||||
@@ -627,11 +622,7 @@ function Option<
|
||||
onPointerLeave: handlePointerLeave,
|
||||
}
|
||||
|
||||
return render<TTag, OptionRenderPropArg>(
|
||||
{ ...passthroughProps, ...propsWeControl },
|
||||
propsBag,
|
||||
DEFAULT_OPTION_TAG
|
||||
)
|
||||
return render({ ...passthroughProps, ...propsWeControl }, propsBag, DEFAULT_OPTION_TAG)
|
||||
}
|
||||
|
||||
function resolvePropValue<TProperty, TBag>(property: TProperty, bag: TBag) {
|
||||
|
||||
@@ -68,10 +68,10 @@ describe('Safe guards', () => {
|
||||
)
|
||||
|
||||
assertMenuButton({
|
||||
state: MenuState.Closed,
|
||||
state: MenuState.InvisibleUnmounted,
|
||||
attributes: { id: 'headlessui-menu-button-1' },
|
||||
})
|
||||
assertMenu({ state: MenuState.Closed })
|
||||
assertMenu({ state: MenuState.InvisibleUnmounted })
|
||||
})
|
||||
)
|
||||
})
|
||||
@@ -99,18 +99,18 @@ describe('Rendering', () => {
|
||||
)
|
||||
|
||||
assertMenuButton({
|
||||
state: MenuState.Closed,
|
||||
state: MenuState.InvisibleUnmounted,
|
||||
attributes: { id: 'headlessui-menu-button-1' },
|
||||
})
|
||||
assertMenu({ state: MenuState.Closed })
|
||||
assertMenu({ state: MenuState.InvisibleUnmounted })
|
||||
|
||||
await click(getMenuButton())
|
||||
|
||||
assertMenuButton({
|
||||
state: MenuState.Open,
|
||||
state: MenuState.Visible,
|
||||
attributes: { id: 'headlessui-menu-button-1' },
|
||||
})
|
||||
assertMenu({ state: MenuState.Open })
|
||||
assertMenu({ state: MenuState.Visible })
|
||||
})
|
||||
)
|
||||
})
|
||||
@@ -131,20 +131,20 @@ describe('Rendering', () => {
|
||||
)
|
||||
|
||||
assertMenuButton({
|
||||
state: MenuState.Closed,
|
||||
state: MenuState.InvisibleUnmounted,
|
||||
attributes: { id: 'headlessui-menu-button-1' },
|
||||
textContent: JSON.stringify({ open: false, focused: false }),
|
||||
})
|
||||
assertMenu({ state: MenuState.Closed })
|
||||
assertMenu({ state: MenuState.InvisibleUnmounted })
|
||||
|
||||
await click(getMenuButton())
|
||||
|
||||
assertMenuButton({
|
||||
state: MenuState.Open,
|
||||
state: MenuState.Visible,
|
||||
attributes: { id: 'headlessui-menu-button-1' },
|
||||
textContent: JSON.stringify({ open: true, focused: false }),
|
||||
})
|
||||
assertMenu({ state: MenuState.Open })
|
||||
assertMenu({ state: MenuState.Visible })
|
||||
})
|
||||
)
|
||||
|
||||
@@ -165,20 +165,20 @@ describe('Rendering', () => {
|
||||
)
|
||||
|
||||
assertMenuButton({
|
||||
state: MenuState.Closed,
|
||||
state: MenuState.InvisibleUnmounted,
|
||||
attributes: { id: 'headlessui-menu-button-1' },
|
||||
textContent: JSON.stringify({ open: false, focused: false }),
|
||||
})
|
||||
assertMenu({ state: MenuState.Closed })
|
||||
assertMenu({ state: MenuState.InvisibleUnmounted })
|
||||
|
||||
await click(getMenuButton())
|
||||
|
||||
assertMenuButton({
|
||||
state: MenuState.Open,
|
||||
state: MenuState.Visible,
|
||||
attributes: { id: 'headlessui-menu-button-1' },
|
||||
textContent: JSON.stringify({ open: true, focused: false }),
|
||||
})
|
||||
assertMenu({ state: MenuState.Open })
|
||||
assertMenu({ state: MenuState.Visible })
|
||||
})
|
||||
)
|
||||
})
|
||||
@@ -201,19 +201,19 @@ describe('Rendering', () => {
|
||||
)
|
||||
|
||||
assertMenuButton({
|
||||
state: MenuState.Closed,
|
||||
state: MenuState.InvisibleUnmounted,
|
||||
attributes: { id: 'headlessui-menu-button-1' },
|
||||
})
|
||||
assertMenu({ state: MenuState.Closed })
|
||||
assertMenu({ state: MenuState.InvisibleUnmounted })
|
||||
|
||||
await click(getMenuButton())
|
||||
|
||||
assertMenuButton({
|
||||
state: MenuState.Open,
|
||||
state: MenuState.Visible,
|
||||
attributes: { id: 'headlessui-menu-button-1' },
|
||||
})
|
||||
assertMenu({
|
||||
state: MenuState.Open,
|
||||
state: MenuState.Visible,
|
||||
textContent: JSON.stringify({ open: true }),
|
||||
})
|
||||
})
|
||||
@@ -234,6 +234,26 @@ describe('Rendering', () => {
|
||||
// Let's verify that the Menu is already there
|
||||
expect(getMenu()).not.toBe(null)
|
||||
})
|
||||
|
||||
it('should be possible to use a different render strategy for the Menu.Items', async () => {
|
||||
render(
|
||||
<Menu>
|
||||
<Menu.Button>Trigger</Menu.Button>
|
||||
<Menu.Items unmount={false}>
|
||||
<Menu.Item as="a">Item A</Menu.Item>
|
||||
<Menu.Item as="a">Item B</Menu.Item>
|
||||
<Menu.Item as="a">Item C</Menu.Item>
|
||||
</Menu.Items>
|
||||
</Menu>
|
||||
)
|
||||
|
||||
assertMenu({ state: MenuState.InvisibleHidden })
|
||||
|
||||
// Let's open the Menu, to see if it is not hidden anymore
|
||||
await click(getMenuButton())
|
||||
|
||||
assertMenu({ state: MenuState.Visible })
|
||||
})
|
||||
})
|
||||
|
||||
describe('Menu.Item', () => {
|
||||
@@ -250,19 +270,19 @@ describe('Rendering', () => {
|
||||
)
|
||||
|
||||
assertMenuButton({
|
||||
state: MenuState.Closed,
|
||||
state: MenuState.InvisibleUnmounted,
|
||||
attributes: { id: 'headlessui-menu-button-1' },
|
||||
})
|
||||
assertMenu({ state: MenuState.Closed })
|
||||
assertMenu({ state: MenuState.InvisibleUnmounted })
|
||||
|
||||
await click(getMenuButton())
|
||||
|
||||
assertMenuButton({
|
||||
state: MenuState.Open,
|
||||
state: MenuState.Visible,
|
||||
attributes: { id: 'headlessui-menu-button-1' },
|
||||
})
|
||||
assertMenu({
|
||||
state: MenuState.Open,
|
||||
state: MenuState.Visible,
|
||||
textContent: JSON.stringify({ active: false, disabled: false }),
|
||||
})
|
||||
})
|
||||
@@ -292,10 +312,10 @@ describe('Rendering composition', () => {
|
||||
)
|
||||
|
||||
assertMenuButton({
|
||||
state: MenuState.Closed,
|
||||
state: MenuState.InvisibleUnmounted,
|
||||
attributes: { id: 'headlessui-menu-button-1' },
|
||||
})
|
||||
assertMenu({ state: MenuState.Closed })
|
||||
assertMenu({ state: MenuState.InvisibleUnmounted })
|
||||
|
||||
// Open menu
|
||||
await click(getMenuButton())
|
||||
@@ -349,10 +369,10 @@ describe('Rendering composition', () => {
|
||||
)
|
||||
|
||||
assertMenuButton({
|
||||
state: MenuState.Closed,
|
||||
state: MenuState.InvisibleUnmounted,
|
||||
attributes: { id: 'headlessui-menu-button-1' },
|
||||
})
|
||||
assertMenu({ state: MenuState.Closed })
|
||||
assertMenu({ state: MenuState.InvisibleUnmounted })
|
||||
|
||||
// Open menu
|
||||
await click(getMenuButton())
|
||||
@@ -381,10 +401,10 @@ describe('Keyboard interactions', () => {
|
||||
)
|
||||
|
||||
assertMenuButton({
|
||||
state: MenuState.Closed,
|
||||
state: MenuState.InvisibleUnmounted,
|
||||
attributes: { id: 'headlessui-menu-button-1' },
|
||||
})
|
||||
assertMenu({ state: MenuState.Closed })
|
||||
assertMenu({ state: MenuState.InvisibleUnmounted })
|
||||
|
||||
// Focus the button
|
||||
getMenuButton()?.focus()
|
||||
@@ -393,9 +413,9 @@ describe('Keyboard interactions', () => {
|
||||
await press(Keys.Enter)
|
||||
|
||||
// Verify it is open
|
||||
assertMenuButton({ state: MenuState.Open })
|
||||
assertMenuButton({ state: MenuState.Visible })
|
||||
assertMenu({
|
||||
state: MenuState.Open,
|
||||
state: MenuState.Visible,
|
||||
attributes: { id: 'headlessui-menu-items-2' },
|
||||
})
|
||||
assertMenuButtonLinkedWithMenu()
|
||||
@@ -425,10 +445,10 @@ describe('Keyboard interactions', () => {
|
||||
)
|
||||
|
||||
assertMenuButton({
|
||||
state: MenuState.Closed,
|
||||
state: MenuState.InvisibleUnmounted,
|
||||
attributes: { id: 'headlessui-menu-button-1' },
|
||||
})
|
||||
assertMenu({ state: MenuState.Closed })
|
||||
assertMenu({ state: MenuState.InvisibleUnmounted })
|
||||
|
||||
// Focus the button
|
||||
getMenuButton()?.focus()
|
||||
@@ -438,10 +458,10 @@ describe('Keyboard interactions', () => {
|
||||
|
||||
// Verify it is still closed
|
||||
assertMenuButton({
|
||||
state: MenuState.Closed,
|
||||
state: MenuState.InvisibleUnmounted,
|
||||
attributes: { id: 'headlessui-menu-button-1' },
|
||||
})
|
||||
assertMenu({ state: MenuState.Closed })
|
||||
assertMenu({ state: MenuState.InvisibleUnmounted })
|
||||
})
|
||||
)
|
||||
|
||||
@@ -455,14 +475,14 @@ describe('Keyboard interactions', () => {
|
||||
</Menu>
|
||||
)
|
||||
|
||||
assertMenu({ state: MenuState.Closed })
|
||||
assertMenu({ state: MenuState.InvisibleUnmounted })
|
||||
|
||||
// Focus the button
|
||||
getMenuButton()?.focus()
|
||||
|
||||
// Open menu
|
||||
await press(Keys.Enter)
|
||||
assertMenu({ state: MenuState.Open })
|
||||
assertMenu({ state: MenuState.Visible })
|
||||
|
||||
assertNoActiveMenuItem()
|
||||
})
|
||||
@@ -485,10 +505,10 @@ describe('Keyboard interactions', () => {
|
||||
)
|
||||
|
||||
assertMenuButton({
|
||||
state: MenuState.Closed,
|
||||
state: MenuState.InvisibleUnmounted,
|
||||
attributes: { id: 'headlessui-menu-button-1' },
|
||||
})
|
||||
assertMenu({ state: MenuState.Closed })
|
||||
assertMenu({ state: MenuState.InvisibleUnmounted })
|
||||
|
||||
// Focus the button
|
||||
getMenuButton()?.focus()
|
||||
@@ -522,10 +542,10 @@ describe('Keyboard interactions', () => {
|
||||
)
|
||||
|
||||
assertMenuButton({
|
||||
state: MenuState.Closed,
|
||||
state: MenuState.InvisibleUnmounted,
|
||||
attributes: { id: 'headlessui-menu-button-1' },
|
||||
})
|
||||
assertMenu({ state: MenuState.Closed })
|
||||
assertMenu({ state: MenuState.InvisibleUnmounted })
|
||||
|
||||
// Focus the button
|
||||
getMenuButton()?.focus()
|
||||
@@ -561,10 +581,10 @@ describe('Keyboard interactions', () => {
|
||||
)
|
||||
|
||||
assertMenuButton({
|
||||
state: MenuState.Closed,
|
||||
state: MenuState.InvisibleUnmounted,
|
||||
attributes: { id: 'headlessui-menu-button-1' },
|
||||
})
|
||||
assertMenu({ state: MenuState.Closed })
|
||||
assertMenu({ state: MenuState.InvisibleUnmounted })
|
||||
|
||||
// Focus the button
|
||||
getMenuButton()?.focus()
|
||||
@@ -591,23 +611,23 @@ describe('Keyboard interactions', () => {
|
||||
)
|
||||
|
||||
assertMenuButton({
|
||||
state: MenuState.Closed,
|
||||
state: MenuState.InvisibleUnmounted,
|
||||
attributes: { id: 'headlessui-menu-button-1' },
|
||||
})
|
||||
assertMenu({ state: MenuState.Closed })
|
||||
assertMenu({ state: MenuState.InvisibleUnmounted })
|
||||
|
||||
// Open menu
|
||||
await click(getMenuButton())
|
||||
|
||||
// Verify it is open
|
||||
assertMenuButton({ state: MenuState.Open })
|
||||
assertMenuButton({ state: MenuState.Visible })
|
||||
|
||||
// Close menu
|
||||
await press(Keys.Enter)
|
||||
|
||||
// Verify it is closed
|
||||
assertMenuButton({ state: MenuState.Closed })
|
||||
assertMenu({ state: MenuState.Closed })
|
||||
assertMenuButton({ state: MenuState.InvisibleUnmounted })
|
||||
assertMenu({ state: MenuState.InvisibleUnmounted })
|
||||
|
||||
// Verify the button is focused again
|
||||
assertActiveElement(getMenuButton())
|
||||
@@ -632,16 +652,16 @@ describe('Keyboard interactions', () => {
|
||||
)
|
||||
|
||||
assertMenuButton({
|
||||
state: MenuState.Closed,
|
||||
state: MenuState.InvisibleUnmounted,
|
||||
attributes: { id: 'headlessui-menu-button-1' },
|
||||
})
|
||||
assertMenu({ state: MenuState.Closed })
|
||||
assertMenu({ state: MenuState.InvisibleUnmounted })
|
||||
|
||||
// Open menu
|
||||
await click(getMenuButton())
|
||||
|
||||
// Verify it is open
|
||||
assertMenuButton({ state: MenuState.Open })
|
||||
assertMenuButton({ state: MenuState.Visible })
|
||||
|
||||
// Activate the first menu item
|
||||
const items = getMenuItems()
|
||||
@@ -651,8 +671,8 @@ describe('Keyboard interactions', () => {
|
||||
await press(Keys.Enter)
|
||||
|
||||
// Verify it is closed
|
||||
assertMenuButton({ state: MenuState.Closed })
|
||||
assertMenu({ state: MenuState.Closed })
|
||||
assertMenuButton({ state: MenuState.InvisibleUnmounted })
|
||||
assertMenu({ state: MenuState.InvisibleUnmounted })
|
||||
|
||||
// Verify the button is focused again
|
||||
assertActiveElement(getMenuButton())
|
||||
@@ -684,16 +704,16 @@ describe('Keyboard interactions', () => {
|
||||
)
|
||||
|
||||
assertMenuButton({
|
||||
state: MenuState.Closed,
|
||||
state: MenuState.InvisibleUnmounted,
|
||||
attributes: { id: 'headlessui-menu-button-1' },
|
||||
})
|
||||
assertMenu({ state: MenuState.Closed })
|
||||
assertMenu({ state: MenuState.InvisibleUnmounted })
|
||||
|
||||
// Open menu
|
||||
await click(getMenuButton())
|
||||
|
||||
// Verify it is open
|
||||
assertMenuButton({ state: MenuState.Open })
|
||||
assertMenuButton({ state: MenuState.Visible })
|
||||
|
||||
// Activate the second menu item
|
||||
const items = getMenuItems()
|
||||
@@ -703,8 +723,8 @@ describe('Keyboard interactions', () => {
|
||||
await press(Keys.Enter)
|
||||
|
||||
// Verify it is closed
|
||||
assertMenuButton({ state: MenuState.Closed })
|
||||
assertMenu({ state: MenuState.Closed })
|
||||
assertMenuButton({ state: MenuState.InvisibleUnmounted })
|
||||
assertMenu({ state: MenuState.InvisibleUnmounted })
|
||||
|
||||
// Verify the button got "clicked"
|
||||
expect(clickHandler).toHaveBeenCalledTimes(1)
|
||||
@@ -745,10 +765,10 @@ describe('Keyboard interactions', () => {
|
||||
)
|
||||
|
||||
assertMenuButton({
|
||||
state: MenuState.Closed,
|
||||
state: MenuState.InvisibleUnmounted,
|
||||
attributes: { id: 'headlessui-menu-button-1' },
|
||||
})
|
||||
assertMenu({ state: MenuState.Closed })
|
||||
assertMenu({ state: MenuState.InvisibleUnmounted })
|
||||
|
||||
// Focus the button
|
||||
getMenuButton()?.focus()
|
||||
@@ -757,9 +777,9 @@ describe('Keyboard interactions', () => {
|
||||
await press(Keys.Space)
|
||||
|
||||
// Verify it is open
|
||||
assertMenuButton({ state: MenuState.Open })
|
||||
assertMenuButton({ state: MenuState.Visible })
|
||||
assertMenu({
|
||||
state: MenuState.Open,
|
||||
state: MenuState.Visible,
|
||||
attributes: { id: 'headlessui-menu-items-2' },
|
||||
})
|
||||
assertMenuButtonLinkedWithMenu()
|
||||
@@ -787,10 +807,10 @@ describe('Keyboard interactions', () => {
|
||||
)
|
||||
|
||||
assertMenuButton({
|
||||
state: MenuState.Closed,
|
||||
state: MenuState.InvisibleUnmounted,
|
||||
attributes: { id: 'headlessui-menu-button-1' },
|
||||
})
|
||||
assertMenu({ state: MenuState.Closed })
|
||||
assertMenu({ state: MenuState.InvisibleUnmounted })
|
||||
|
||||
// Focus the button
|
||||
getMenuButton()?.focus()
|
||||
@@ -800,10 +820,10 @@ describe('Keyboard interactions', () => {
|
||||
|
||||
// Verify it is still closed
|
||||
assertMenuButton({
|
||||
state: MenuState.Closed,
|
||||
state: MenuState.InvisibleUnmounted,
|
||||
attributes: { id: 'headlessui-menu-button-1' },
|
||||
})
|
||||
assertMenu({ state: MenuState.Closed })
|
||||
assertMenu({ state: MenuState.InvisibleUnmounted })
|
||||
})
|
||||
)
|
||||
|
||||
@@ -817,14 +837,14 @@ describe('Keyboard interactions', () => {
|
||||
</Menu>
|
||||
)
|
||||
|
||||
assertMenu({ state: MenuState.Closed })
|
||||
assertMenu({ state: MenuState.InvisibleUnmounted })
|
||||
|
||||
// Focus the button
|
||||
getMenuButton()?.focus()
|
||||
|
||||
// Open menu
|
||||
await press(Keys.Space)
|
||||
assertMenu({ state: MenuState.Open })
|
||||
assertMenu({ state: MenuState.Visible })
|
||||
|
||||
assertNoActiveMenuItem()
|
||||
})
|
||||
@@ -847,10 +867,10 @@ describe('Keyboard interactions', () => {
|
||||
)
|
||||
|
||||
assertMenuButton({
|
||||
state: MenuState.Closed,
|
||||
state: MenuState.InvisibleUnmounted,
|
||||
attributes: { id: 'headlessui-menu-button-1' },
|
||||
})
|
||||
assertMenu({ state: MenuState.Closed })
|
||||
assertMenu({ state: MenuState.InvisibleUnmounted })
|
||||
|
||||
// Focus the button
|
||||
getMenuButton()?.focus()
|
||||
@@ -884,10 +904,10 @@ describe('Keyboard interactions', () => {
|
||||
)
|
||||
|
||||
assertMenuButton({
|
||||
state: MenuState.Closed,
|
||||
state: MenuState.InvisibleUnmounted,
|
||||
attributes: { id: 'headlessui-menu-button-1' },
|
||||
})
|
||||
assertMenu({ state: MenuState.Closed })
|
||||
assertMenu({ state: MenuState.InvisibleUnmounted })
|
||||
|
||||
// Focus the button
|
||||
getMenuButton()?.focus()
|
||||
@@ -923,10 +943,10 @@ describe('Keyboard interactions', () => {
|
||||
)
|
||||
|
||||
assertMenuButton({
|
||||
state: MenuState.Closed,
|
||||
state: MenuState.InvisibleUnmounted,
|
||||
attributes: { id: 'headlessui-menu-button-1' },
|
||||
})
|
||||
assertMenu({ state: MenuState.Closed })
|
||||
assertMenu({ state: MenuState.InvisibleUnmounted })
|
||||
|
||||
// Focus the button
|
||||
getMenuButton()?.focus()
|
||||
@@ -953,23 +973,23 @@ describe('Keyboard interactions', () => {
|
||||
)
|
||||
|
||||
assertMenuButton({
|
||||
state: MenuState.Closed,
|
||||
state: MenuState.InvisibleUnmounted,
|
||||
attributes: { id: 'headlessui-menu-button-1' },
|
||||
})
|
||||
assertMenu({ state: MenuState.Closed })
|
||||
assertMenu({ state: MenuState.InvisibleUnmounted })
|
||||
|
||||
// Open menu
|
||||
await click(getMenuButton())
|
||||
|
||||
// Verify it is open
|
||||
assertMenuButton({ state: MenuState.Open })
|
||||
assertMenuButton({ state: MenuState.Visible })
|
||||
|
||||
// Close menu
|
||||
await press(Keys.Space)
|
||||
|
||||
// Verify it is closed
|
||||
assertMenuButton({ state: MenuState.Closed })
|
||||
assertMenu({ state: MenuState.Closed })
|
||||
assertMenuButton({ state: MenuState.InvisibleUnmounted })
|
||||
assertMenu({ state: MenuState.InvisibleUnmounted })
|
||||
|
||||
// Verify the button is focused again
|
||||
assertActiveElement(getMenuButton())
|
||||
@@ -994,16 +1014,16 @@ describe('Keyboard interactions', () => {
|
||||
)
|
||||
|
||||
assertMenuButton({
|
||||
state: MenuState.Closed,
|
||||
state: MenuState.InvisibleUnmounted,
|
||||
attributes: { id: 'headlessui-menu-button-1' },
|
||||
})
|
||||
assertMenu({ state: MenuState.Closed })
|
||||
assertMenu({ state: MenuState.InvisibleUnmounted })
|
||||
|
||||
// Open menu
|
||||
await click(getMenuButton())
|
||||
|
||||
// Verify it is open
|
||||
assertMenuButton({ state: MenuState.Open })
|
||||
assertMenuButton({ state: MenuState.Visible })
|
||||
|
||||
// Activate the first menu item
|
||||
const items = getMenuItems()
|
||||
@@ -1013,8 +1033,8 @@ describe('Keyboard interactions', () => {
|
||||
await press(Keys.Space)
|
||||
|
||||
// Verify it is closed
|
||||
assertMenuButton({ state: MenuState.Closed })
|
||||
assertMenu({ state: MenuState.Closed })
|
||||
assertMenuButton({ state: MenuState.InvisibleUnmounted })
|
||||
assertMenu({ state: MenuState.InvisibleUnmounted })
|
||||
|
||||
// Verify the "click" went through on the `a` tag
|
||||
expect(clickHandler).toHaveBeenCalled()
|
||||
@@ -1047,9 +1067,9 @@ describe('Keyboard interactions', () => {
|
||||
await press(Keys.Space)
|
||||
|
||||
// Verify it is open
|
||||
assertMenuButton({ state: MenuState.Open })
|
||||
assertMenuButton({ state: MenuState.Visible })
|
||||
assertMenu({
|
||||
state: MenuState.Open,
|
||||
state: MenuState.Visible,
|
||||
attributes: { id: 'headlessui-menu-items-2' },
|
||||
})
|
||||
assertMenuButtonLinkedWithMenu()
|
||||
@@ -1058,8 +1078,8 @@ describe('Keyboard interactions', () => {
|
||||
await press(Keys.Escape)
|
||||
|
||||
// Verify it is closed
|
||||
assertMenuButton({ state: MenuState.Closed })
|
||||
assertMenu({ state: MenuState.Closed })
|
||||
assertMenuButton({ state: MenuState.InvisibleUnmounted })
|
||||
assertMenu({ state: MenuState.InvisibleUnmounted })
|
||||
|
||||
// Verify the button is focused again
|
||||
assertActiveElement(getMenuButton())
|
||||
@@ -1083,10 +1103,10 @@ describe('Keyboard interactions', () => {
|
||||
)
|
||||
|
||||
assertMenuButton({
|
||||
state: MenuState.Closed,
|
||||
state: MenuState.InvisibleUnmounted,
|
||||
attributes: { id: 'headlessui-menu-button-1' },
|
||||
})
|
||||
assertMenu({ state: MenuState.Closed })
|
||||
assertMenu({ state: MenuState.InvisibleUnmounted })
|
||||
|
||||
// Focus the button
|
||||
getMenuButton()?.focus()
|
||||
@@ -1095,9 +1115,9 @@ describe('Keyboard interactions', () => {
|
||||
await press(Keys.Enter)
|
||||
|
||||
// Verify it is open
|
||||
assertMenuButton({ state: MenuState.Open })
|
||||
assertMenuButton({ state: MenuState.Visible })
|
||||
assertMenu({
|
||||
state: MenuState.Open,
|
||||
state: MenuState.Visible,
|
||||
attributes: { id: 'headlessui-menu-items-2' },
|
||||
})
|
||||
assertMenuButtonLinkedWithMenu()
|
||||
@@ -1112,8 +1132,8 @@ describe('Keyboard interactions', () => {
|
||||
await press(Keys.Tab)
|
||||
|
||||
// Verify it is still open
|
||||
assertMenuButton({ state: MenuState.Open })
|
||||
assertMenu({ state: MenuState.Open })
|
||||
assertMenuButton({ state: MenuState.Visible })
|
||||
assertMenu({ state: MenuState.Visible })
|
||||
})
|
||||
)
|
||||
|
||||
@@ -1132,10 +1152,10 @@ describe('Keyboard interactions', () => {
|
||||
)
|
||||
|
||||
assertMenuButton({
|
||||
state: MenuState.Closed,
|
||||
state: MenuState.InvisibleUnmounted,
|
||||
attributes: { id: 'headlessui-menu-button-1' },
|
||||
})
|
||||
assertMenu({ state: MenuState.Closed })
|
||||
assertMenu({ state: MenuState.InvisibleUnmounted })
|
||||
|
||||
// Focus the button
|
||||
getMenuButton()?.focus()
|
||||
@@ -1144,9 +1164,9 @@ describe('Keyboard interactions', () => {
|
||||
await press(Keys.Enter)
|
||||
|
||||
// Verify it is open
|
||||
assertMenuButton({ state: MenuState.Open })
|
||||
assertMenuButton({ state: MenuState.Visible })
|
||||
assertMenu({
|
||||
state: MenuState.Open,
|
||||
state: MenuState.Visible,
|
||||
attributes: { id: 'headlessui-menu-items-2' },
|
||||
})
|
||||
assertMenuButtonLinkedWithMenu()
|
||||
@@ -1161,8 +1181,8 @@ describe('Keyboard interactions', () => {
|
||||
await press(shift(Keys.Tab))
|
||||
|
||||
// Verify it is still open
|
||||
assertMenuButton({ state: MenuState.Open })
|
||||
assertMenu({ state: MenuState.Open })
|
||||
assertMenuButton({ state: MenuState.Visible })
|
||||
assertMenu({ state: MenuState.Visible })
|
||||
})
|
||||
)
|
||||
})
|
||||
@@ -1183,10 +1203,10 @@ describe('Keyboard interactions', () => {
|
||||
)
|
||||
|
||||
assertMenuButton({
|
||||
state: MenuState.Closed,
|
||||
state: MenuState.InvisibleUnmounted,
|
||||
attributes: { id: 'headlessui-menu-button-1' },
|
||||
})
|
||||
assertMenu({ state: MenuState.Closed })
|
||||
assertMenu({ state: MenuState.InvisibleUnmounted })
|
||||
|
||||
// Focus the button
|
||||
getMenuButton()?.focus()
|
||||
@@ -1195,9 +1215,9 @@ describe('Keyboard interactions', () => {
|
||||
await press(Keys.ArrowDown)
|
||||
|
||||
// Verify it is open
|
||||
assertMenuButton({ state: MenuState.Open })
|
||||
assertMenuButton({ state: MenuState.Visible })
|
||||
assertMenu({
|
||||
state: MenuState.Open,
|
||||
state: MenuState.Visible,
|
||||
attributes: { id: 'headlessui-menu-items-2' },
|
||||
})
|
||||
assertMenuButtonLinkedWithMenu()
|
||||
@@ -1227,10 +1247,10 @@ describe('Keyboard interactions', () => {
|
||||
)
|
||||
|
||||
assertMenuButton({
|
||||
state: MenuState.Closed,
|
||||
state: MenuState.InvisibleUnmounted,
|
||||
attributes: { id: 'headlessui-menu-button-1' },
|
||||
})
|
||||
assertMenu({ state: MenuState.Closed })
|
||||
assertMenu({ state: MenuState.InvisibleUnmounted })
|
||||
|
||||
// Focus the button
|
||||
getMenuButton()?.focus()
|
||||
@@ -1240,10 +1260,10 @@ describe('Keyboard interactions', () => {
|
||||
|
||||
// Verify it is still closed
|
||||
assertMenuButton({
|
||||
state: MenuState.Closed,
|
||||
state: MenuState.InvisibleUnmounted,
|
||||
attributes: { id: 'headlessui-menu-button-1' },
|
||||
})
|
||||
assertMenu({ state: MenuState.Closed })
|
||||
assertMenu({ state: MenuState.InvisibleUnmounted })
|
||||
})
|
||||
)
|
||||
|
||||
@@ -1257,14 +1277,14 @@ describe('Keyboard interactions', () => {
|
||||
</Menu>
|
||||
)
|
||||
|
||||
assertMenu({ state: MenuState.Closed })
|
||||
assertMenu({ state: MenuState.InvisibleUnmounted })
|
||||
|
||||
// Focus the button
|
||||
getMenuButton()?.focus()
|
||||
|
||||
// Open menu
|
||||
await press(Keys.ArrowDown)
|
||||
assertMenu({ state: MenuState.Open })
|
||||
assertMenu({ state: MenuState.Visible })
|
||||
|
||||
assertNoActiveMenuItem()
|
||||
})
|
||||
@@ -1285,10 +1305,10 @@ describe('Keyboard interactions', () => {
|
||||
)
|
||||
|
||||
assertMenuButton({
|
||||
state: MenuState.Closed,
|
||||
state: MenuState.InvisibleUnmounted,
|
||||
attributes: { id: 'headlessui-menu-button-1' },
|
||||
})
|
||||
assertMenu({ state: MenuState.Closed })
|
||||
assertMenu({ state: MenuState.InvisibleUnmounted })
|
||||
|
||||
// Focus the button
|
||||
getMenuButton()?.focus()
|
||||
@@ -1333,10 +1353,10 @@ describe('Keyboard interactions', () => {
|
||||
)
|
||||
|
||||
assertMenuButton({
|
||||
state: MenuState.Closed,
|
||||
state: MenuState.InvisibleUnmounted,
|
||||
attributes: { id: 'headlessui-menu-button-1' },
|
||||
})
|
||||
assertMenu({ state: MenuState.Closed })
|
||||
assertMenu({ state: MenuState.InvisibleUnmounted })
|
||||
|
||||
// Focus the button
|
||||
getMenuButton()?.focus()
|
||||
@@ -1375,10 +1395,10 @@ describe('Keyboard interactions', () => {
|
||||
)
|
||||
|
||||
assertMenuButton({
|
||||
state: MenuState.Closed,
|
||||
state: MenuState.InvisibleUnmounted,
|
||||
attributes: { id: 'headlessui-menu-button-1' },
|
||||
})
|
||||
assertMenu({ state: MenuState.Closed })
|
||||
assertMenu({ state: MenuState.InvisibleUnmounted })
|
||||
|
||||
// Focus the button
|
||||
getMenuButton()?.focus()
|
||||
@@ -1411,10 +1431,10 @@ describe('Keyboard interactions', () => {
|
||||
)
|
||||
|
||||
assertMenuButton({
|
||||
state: MenuState.Closed,
|
||||
state: MenuState.InvisibleUnmounted,
|
||||
attributes: { id: 'headlessui-menu-button-1' },
|
||||
})
|
||||
assertMenu({ state: MenuState.Closed })
|
||||
assertMenu({ state: MenuState.InvisibleUnmounted })
|
||||
|
||||
// Focus the button
|
||||
getMenuButton()?.focus()
|
||||
@@ -1423,9 +1443,9 @@ describe('Keyboard interactions', () => {
|
||||
await press(Keys.ArrowUp)
|
||||
|
||||
// Verify it is open
|
||||
assertMenuButton({ state: MenuState.Open })
|
||||
assertMenuButton({ state: MenuState.Visible })
|
||||
assertMenu({
|
||||
state: MenuState.Open,
|
||||
state: MenuState.Visible,
|
||||
attributes: { id: 'headlessui-menu-items-2' },
|
||||
})
|
||||
assertMenuButtonLinkedWithMenu()
|
||||
@@ -1455,10 +1475,10 @@ describe('Keyboard interactions', () => {
|
||||
)
|
||||
|
||||
assertMenuButton({
|
||||
state: MenuState.Closed,
|
||||
state: MenuState.InvisibleUnmounted,
|
||||
attributes: { id: 'headlessui-menu-button-1' },
|
||||
})
|
||||
assertMenu({ state: MenuState.Closed })
|
||||
assertMenu({ state: MenuState.InvisibleUnmounted })
|
||||
|
||||
// Focus the button
|
||||
getMenuButton()?.focus()
|
||||
@@ -1468,10 +1488,10 @@ describe('Keyboard interactions', () => {
|
||||
|
||||
// Verify it is still closed
|
||||
assertMenuButton({
|
||||
state: MenuState.Closed,
|
||||
state: MenuState.InvisibleUnmounted,
|
||||
attributes: { id: 'headlessui-menu-button-1' },
|
||||
})
|
||||
assertMenu({ state: MenuState.Closed })
|
||||
assertMenu({ state: MenuState.InvisibleUnmounted })
|
||||
})
|
||||
)
|
||||
|
||||
@@ -1485,14 +1505,14 @@ describe('Keyboard interactions', () => {
|
||||
</Menu>
|
||||
)
|
||||
|
||||
assertMenu({ state: MenuState.Closed })
|
||||
assertMenu({ state: MenuState.InvisibleUnmounted })
|
||||
|
||||
// Focus the button
|
||||
getMenuButton()?.focus()
|
||||
|
||||
// Open menu
|
||||
await press(Keys.ArrowUp)
|
||||
assertMenu({ state: MenuState.Open })
|
||||
assertMenu({ state: MenuState.Visible })
|
||||
|
||||
assertNoActiveMenuItem()
|
||||
})
|
||||
@@ -1517,10 +1537,10 @@ describe('Keyboard interactions', () => {
|
||||
)
|
||||
|
||||
assertMenuButton({
|
||||
state: MenuState.Closed,
|
||||
state: MenuState.InvisibleUnmounted,
|
||||
attributes: { id: 'headlessui-menu-button-1' },
|
||||
})
|
||||
assertMenu({ state: MenuState.Closed })
|
||||
assertMenu({ state: MenuState.InvisibleUnmounted })
|
||||
|
||||
// Focus the button
|
||||
getMenuButton()?.focus()
|
||||
@@ -1555,10 +1575,10 @@ describe('Keyboard interactions', () => {
|
||||
)
|
||||
|
||||
assertMenuButton({
|
||||
state: MenuState.Closed,
|
||||
state: MenuState.InvisibleUnmounted,
|
||||
attributes: { id: 'headlessui-menu-button-1' },
|
||||
})
|
||||
assertMenu({ state: MenuState.Closed })
|
||||
assertMenu({ state: MenuState.InvisibleUnmounted })
|
||||
|
||||
// Focus the button
|
||||
getMenuButton()?.focus()
|
||||
@@ -1597,10 +1617,10 @@ describe('Keyboard interactions', () => {
|
||||
)
|
||||
|
||||
assertMenuButton({
|
||||
state: MenuState.Closed,
|
||||
state: MenuState.InvisibleUnmounted,
|
||||
attributes: { id: 'headlessui-menu-button-1' },
|
||||
})
|
||||
assertMenu({ state: MenuState.Closed })
|
||||
assertMenu({ state: MenuState.InvisibleUnmounted })
|
||||
|
||||
// Focus the button
|
||||
getMenuButton()?.focus()
|
||||
@@ -1609,9 +1629,9 @@ describe('Keyboard interactions', () => {
|
||||
await press(Keys.ArrowUp)
|
||||
|
||||
// Verify it is open
|
||||
assertMenuButton({ state: MenuState.Open })
|
||||
assertMenuButton({ state: MenuState.Visible })
|
||||
assertMenu({
|
||||
state: MenuState.Open,
|
||||
state: MenuState.Visible,
|
||||
attributes: { id: 'headlessui-menu-items-2' },
|
||||
})
|
||||
assertMenuButtonLinkedWithMenu()
|
||||
@@ -2360,18 +2380,18 @@ describe('Mouse interactions', () => {
|
||||
)
|
||||
|
||||
assertMenuButton({
|
||||
state: MenuState.Closed,
|
||||
state: MenuState.InvisibleUnmounted,
|
||||
attributes: { id: 'headlessui-menu-button-1' },
|
||||
})
|
||||
assertMenu({ state: MenuState.Closed })
|
||||
assertMenu({ state: MenuState.InvisibleUnmounted })
|
||||
|
||||
// Open menu
|
||||
await click(getMenuButton())
|
||||
|
||||
// Verify it is open
|
||||
assertMenuButton({ state: MenuState.Open })
|
||||
assertMenuButton({ state: MenuState.Visible })
|
||||
assertMenu({
|
||||
state: MenuState.Open,
|
||||
state: MenuState.Visible,
|
||||
attributes: { id: 'headlessui-menu-items-2' },
|
||||
})
|
||||
assertMenuButtonLinkedWithMenu()
|
||||
@@ -2398,20 +2418,20 @@ describe('Mouse interactions', () => {
|
||||
)
|
||||
|
||||
assertMenuButton({
|
||||
state: MenuState.Closed,
|
||||
state: MenuState.InvisibleUnmounted,
|
||||
attributes: { id: 'headlessui-menu-button-1' },
|
||||
})
|
||||
assertMenu({ state: MenuState.Closed })
|
||||
assertMenu({ state: MenuState.InvisibleUnmounted })
|
||||
|
||||
// Try to open the menu
|
||||
await click(getMenuButton())
|
||||
|
||||
// Verify it is still closed
|
||||
assertMenuButton({
|
||||
state: MenuState.Closed,
|
||||
state: MenuState.InvisibleUnmounted,
|
||||
attributes: { id: 'headlessui-menu-button-1' },
|
||||
})
|
||||
assertMenu({ state: MenuState.Closed })
|
||||
assertMenu({ state: MenuState.InvisibleUnmounted })
|
||||
})
|
||||
)
|
||||
|
||||
@@ -2433,14 +2453,14 @@ describe('Mouse interactions', () => {
|
||||
await click(getMenuButton())
|
||||
|
||||
// Verify it is open
|
||||
assertMenuButton({ state: MenuState.Open })
|
||||
assertMenuButton({ state: MenuState.Visible })
|
||||
|
||||
// Click to close
|
||||
await click(getMenuButton())
|
||||
|
||||
// Verify it is closed
|
||||
assertMenuButton({ state: MenuState.Closed })
|
||||
assertMenu({ state: MenuState.Closed })
|
||||
assertMenuButton({ state: MenuState.InvisibleUnmounted })
|
||||
assertMenu({ state: MenuState.InvisibleUnmounted })
|
||||
})
|
||||
)
|
||||
|
||||
@@ -2484,13 +2504,13 @@ describe('Mouse interactions', () => {
|
||||
)
|
||||
|
||||
// Verify that the window is closed
|
||||
assertMenu({ state: MenuState.Closed })
|
||||
assertMenu({ state: MenuState.InvisibleUnmounted })
|
||||
|
||||
// Click something that is not related to the menu
|
||||
await click(document.body)
|
||||
|
||||
// Should still be closed
|
||||
assertMenu({ state: MenuState.Closed })
|
||||
assertMenu({ state: MenuState.InvisibleUnmounted })
|
||||
})
|
||||
)
|
||||
|
||||
@@ -2510,13 +2530,13 @@ describe('Mouse interactions', () => {
|
||||
|
||||
// Open menu
|
||||
await click(getMenuButton())
|
||||
assertMenu({ state: MenuState.Open })
|
||||
assertMenu({ state: MenuState.Visible })
|
||||
|
||||
// Click something that is not related to the menu
|
||||
await click(document.body)
|
||||
|
||||
// Should be closed now
|
||||
assertMenu({ state: MenuState.Closed })
|
||||
assertMenu({ state: MenuState.InvisibleUnmounted })
|
||||
|
||||
// Verify the button is focused again
|
||||
assertActiveElement(getMenuButton())
|
||||
@@ -2539,13 +2559,13 @@ describe('Mouse interactions', () => {
|
||||
|
||||
// Open menu
|
||||
await click(getMenuButton())
|
||||
assertMenu({ state: MenuState.Open })
|
||||
assertMenu({ state: MenuState.Visible })
|
||||
|
||||
// Click the menu button again
|
||||
await click(getMenuButton())
|
||||
|
||||
// Should be closed now
|
||||
assertMenu({ state: MenuState.Closed })
|
||||
assertMenu({ state: MenuState.InvisibleUnmounted })
|
||||
|
||||
// Verify the button is focused again
|
||||
assertActiveElement(getMenuButton())
|
||||
@@ -2828,14 +2848,14 @@ describe('Mouse interactions', () => {
|
||||
|
||||
// Open menu
|
||||
await click(getMenuButton())
|
||||
assertMenu({ state: MenuState.Open })
|
||||
assertMenu({ state: MenuState.Visible })
|
||||
|
||||
const items = getMenuItems()
|
||||
|
||||
// We should be able to click the first item
|
||||
await click(items[1])
|
||||
|
||||
assertMenu({ state: MenuState.Closed })
|
||||
assertMenu({ state: MenuState.InvisibleUnmounted })
|
||||
expect(clickHandler).toHaveBeenCalled()
|
||||
})
|
||||
)
|
||||
@@ -2861,11 +2881,11 @@ describe('Mouse interactions', () => {
|
||||
|
||||
// Open menu
|
||||
await click(getMenuButton())
|
||||
assertMenu({ state: MenuState.Open })
|
||||
assertMenu({ state: MenuState.Visible })
|
||||
|
||||
// We should be able to click the first item
|
||||
await click(getMenuItems()[1])
|
||||
assertMenu({ state: MenuState.Closed })
|
||||
assertMenu({ state: MenuState.InvisibleUnmounted })
|
||||
|
||||
// Verify the callback has been called
|
||||
expect(clickHandler).toHaveBeenCalledTimes(1)
|
||||
@@ -2875,7 +2895,7 @@ describe('Mouse interactions', () => {
|
||||
|
||||
// Click the last item, which should close and invoke the handler
|
||||
await click(getMenuItems()[2])
|
||||
assertMenu({ state: MenuState.Closed })
|
||||
assertMenu({ state: MenuState.InvisibleUnmounted })
|
||||
|
||||
// Verify the callback has been called
|
||||
expect(clickHandler).toHaveBeenCalledTimes(2)
|
||||
@@ -2900,13 +2920,13 @@ describe('Mouse interactions', () => {
|
||||
|
||||
// Open menu
|
||||
await click(getMenuButton())
|
||||
assertMenu({ state: MenuState.Open })
|
||||
assertMenu({ state: MenuState.Visible })
|
||||
|
||||
const items = getMenuItems()
|
||||
|
||||
// We should be able to click the first item
|
||||
await click(items[1])
|
||||
assertMenu({ state: MenuState.Open })
|
||||
assertMenu({ state: MenuState.Visible })
|
||||
})
|
||||
)
|
||||
|
||||
@@ -2926,7 +2946,7 @@ describe('Mouse interactions', () => {
|
||||
|
||||
// Open menu
|
||||
await click(getMenuButton())
|
||||
assertMenu({ state: MenuState.Open })
|
||||
assertMenu({ state: MenuState.Visible })
|
||||
|
||||
const items = getMenuItems()
|
||||
|
||||
@@ -2957,7 +2977,7 @@ describe('Mouse interactions', () => {
|
||||
|
||||
// Open menu
|
||||
await click(getMenuButton())
|
||||
assertMenu({ state: MenuState.Open })
|
||||
assertMenu({ state: MenuState.Visible })
|
||||
|
||||
const items = getMenuItems()
|
||||
|
||||
@@ -2991,7 +3011,7 @@ describe('Mouse interactions', () => {
|
||||
|
||||
// Open menu
|
||||
await click(getMenuButton())
|
||||
assertMenu({ state: MenuState.Open })
|
||||
assertMenu({ state: MenuState.Visible })
|
||||
|
||||
const items = getMenuItems()
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import * as React from 'react'
|
||||
|
||||
import { Props } from '../../types'
|
||||
import { match } from '../../utils/match'
|
||||
import { forwardRefWithAs, render } from '../../utils/render'
|
||||
import { forwardRefWithAs, render, Features, PropsForFeatures } from '../../utils/render'
|
||||
import { disposables } from '../../utils/disposables'
|
||||
import { useDisposables } from '../../hooks/use-disposables'
|
||||
import { useIsoMorphicEffect } from '../../hooks/use-iso-morphic-effect'
|
||||
@@ -108,7 +108,11 @@ const reducers: {
|
||||
action: Extract<Actions, { type: P }>
|
||||
) => StateDefinition
|
||||
} = {
|
||||
[ActionTypes.CloseMenu]: state => ({ ...state, menuState: MenuStates.Closed }),
|
||||
[ActionTypes.CloseMenu]: state => ({
|
||||
...state,
|
||||
activeItemIndex: null,
|
||||
menuState: MenuStates.Closed,
|
||||
}),
|
||||
[ActionTypes.OpenMenu]: state => ({ ...state, menuState: MenuStates.Open }),
|
||||
[ActionTypes.GoToItem]: (state, action) => {
|
||||
const activeItemIndex = calculateActiveItemIndex(state, action.focus, action.id)
|
||||
@@ -348,14 +352,15 @@ type ItemsPropsWeControl =
|
||||
const DEFAULT_ITEMS_TAG = 'div'
|
||||
|
||||
type ItemsRenderPropArg = { open: boolean }
|
||||
const ItemsRenderFeatures = Features.RenderStrategy | Features.Static
|
||||
|
||||
const Items = forwardRefWithAs(function Items<
|
||||
TTag extends React.ElementType = typeof DEFAULT_ITEMS_TAG
|
||||
>(
|
||||
props: Props<TTag, ItemsRenderPropArg, ItemsPropsWeControl> & { static?: boolean },
|
||||
props: Props<TTag, ItemsRenderPropArg, ItemsPropsWeControl> &
|
||||
PropsForFeatures<typeof ItemsRenderFeatures>,
|
||||
ref: React.Ref<HTMLDivElement>
|
||||
) {
|
||||
const { static: isStatic = false, ...passthroughProps } = props
|
||||
const [state, dispatch] = useMenuContext([Menu.name, Items.name].join('.'))
|
||||
const itemsRef = useSyncRefs(state.itemsRef, ref)
|
||||
|
||||
@@ -433,14 +438,16 @@ const Items = forwardRefWithAs(function Items<
|
||||
onKeyDown: handleKeyDown,
|
||||
role: 'menu',
|
||||
tabIndex: 0,
|
||||
ref: itemsRef,
|
||||
}
|
||||
|
||||
if (!isStatic && state.menuState === MenuStates.Closed) return null
|
||||
const passthroughProps = props
|
||||
|
||||
return render(
|
||||
{ ...passthroughProps, ...propsWeControl, ...{ ref: itemsRef } },
|
||||
{ ...passthroughProps, ...propsWeControl },
|
||||
propsBag,
|
||||
DEFAULT_ITEMS_TAG
|
||||
DEFAULT_ITEMS_TAG,
|
||||
ItemsRenderFeatures,
|
||||
state.menuState === MenuStates.Open
|
||||
)
|
||||
})
|
||||
|
||||
@@ -529,11 +536,7 @@ function Item<TTag extends React.ElementType = typeof DEFAULT_ITEM_TAG>(
|
||||
onPointerLeave: handlePointerLeave,
|
||||
}
|
||||
|
||||
return render<TTag, ItemRenderPropArg>(
|
||||
{ ...passthroughProps, ...propsWeControl },
|
||||
propsBag,
|
||||
DEFAULT_ITEM_TAG
|
||||
)
|
||||
return render({ ...passthroughProps, ...propsWeControl }, propsBag, DEFAULT_ITEM_TAG)
|
||||
}
|
||||
|
||||
function resolvePropValue<TProperty, TBag>(property: TProperty, bag: TBag) {
|
||||
|
||||
@@ -120,7 +120,9 @@ describe('Setup API', () => {
|
||||
|
||||
it('should be possible to use a render prop', () => {
|
||||
const { container } = render(
|
||||
<Transition show={true}>{ref => <span ref={ref}>Children</span>}</Transition>
|
||||
<Transition show={true} as={React.Fragment}>
|
||||
{() => <span>Children</span>}
|
||||
</Transition>
|
||||
)
|
||||
|
||||
expect(container.firstChild).toMatchInlineSnapshot(`
|
||||
@@ -131,12 +133,20 @@ describe('Setup API', () => {
|
||||
})
|
||||
|
||||
it(
|
||||
'should yell at us when we forget to apply the ref when using a render prop',
|
||||
'should yell at us when we forget to forward the ref when using a render prop',
|
||||
suppressConsoleLogs(() => {
|
||||
expect.assertions(1)
|
||||
|
||||
function Dummy(props: any) {
|
||||
return <span {...props}>Children</span>
|
||||
}
|
||||
|
||||
expect(() => {
|
||||
render(<Transition show={true}>{() => <span>Children</span>}</Transition>)
|
||||
render(
|
||||
<Transition show={true} as={React.Fragment}>
|
||||
{() => <Dummy />}
|
||||
</Transition>
|
||||
)
|
||||
}).toThrowErrorMatchingInlineSnapshot(
|
||||
`"Did you forget to passthrough the \`ref\` to the actual DOM node?"`
|
||||
)
|
||||
@@ -253,8 +263,10 @@ describe('Setup API', () => {
|
||||
const { container } = render(
|
||||
<div className="My Page">
|
||||
<Transition show={true}>
|
||||
<Transition.Child>{ref => <aside ref={ref}>Sidebar</aside>}</Transition.Child>
|
||||
<Transition.Child>{ref => <section ref={ref}>Content</section>}</Transition.Child>
|
||||
<Transition.Child as={React.Fragment}>{() => <aside>Sidebar</aside>}</Transition.Child>
|
||||
<Transition.Child as={React.Fragment}>
|
||||
{() => <section>Content</section>}
|
||||
</Transition.Child>
|
||||
</Transition>
|
||||
</div>
|
||||
)
|
||||
@@ -278,11 +290,15 @@ describe('Setup API', () => {
|
||||
it('should be possible to use render props on the Transition and Transition.Child components', () => {
|
||||
const { container } = render(
|
||||
<div className="My Page">
|
||||
<Transition show={true}>
|
||||
{ref => (
|
||||
<article ref={ref}>
|
||||
<Transition.Child>{ref => <aside ref={ref}>Sidebar</aside>}</Transition.Child>
|
||||
<Transition.Child>{ref => <section ref={ref}>Content</section>}</Transition.Child>
|
||||
<Transition show={true} as={React.Fragment}>
|
||||
{() => (
|
||||
<article>
|
||||
<Transition.Child as={React.Fragment}>
|
||||
{() => <aside>Sidebar</aside>}
|
||||
</Transition.Child>
|
||||
<Transition.Child as={React.Fragment}>
|
||||
{() => <section>Content</section>}
|
||||
</Transition.Child>
|
||||
</article>
|
||||
)}
|
||||
</Transition>
|
||||
@@ -306,16 +322,24 @@ describe('Setup API', () => {
|
||||
})
|
||||
|
||||
it(
|
||||
'should yell at us when we forgot to apply the ref on one of the Transition.Child components',
|
||||
'should yell at us when we forgot to forward the ref on one of the Transition.Child components',
|
||||
suppressConsoleLogs(() => {
|
||||
expect.assertions(1)
|
||||
|
||||
function Dummy(props: any) {
|
||||
return <div {...props} />
|
||||
}
|
||||
|
||||
expect(() => {
|
||||
render(
|
||||
<div className="My Page">
|
||||
<Transition show={true}>
|
||||
<Transition.Child>{ref => <aside ref={ref}>Sidebar</aside>}</Transition.Child>
|
||||
<Transition.Child>{() => <section>Content</section>}</Transition.Child>
|
||||
<Transition.Child as={React.Fragment}>
|
||||
{() => <Dummy>Sidebar</Dummy>}
|
||||
</Transition.Child>
|
||||
<Transition.Child as={React.Fragment}>
|
||||
{() => <Dummy>Content</Dummy>}
|
||||
</Transition.Child>
|
||||
</Transition>
|
||||
</div>
|
||||
)
|
||||
@@ -326,21 +350,23 @@ describe('Setup API', () => {
|
||||
)
|
||||
|
||||
it(
|
||||
'should yell at us when we forgot to apply a ref on the Transition component',
|
||||
'should yell at us when we forgot to forward a ref on the Transition component',
|
||||
suppressConsoleLogs(() => {
|
||||
expect.assertions(1)
|
||||
|
||||
function Dummy(props: any) {
|
||||
return <div {...props} />
|
||||
}
|
||||
|
||||
expect(() => {
|
||||
render(
|
||||
<div className="My Page">
|
||||
<Transition show={true}>
|
||||
<Transition show={true} as={React.Fragment}>
|
||||
{() => (
|
||||
<article>
|
||||
<Transition.Child>{ref => <aside ref={ref}>Sidebar</aside>}</Transition.Child>
|
||||
<Transition.Child>
|
||||
{ref => <section ref={ref}>Content</section>}
|
||||
</Transition.Child>
|
||||
</article>
|
||||
<Dummy>
|
||||
<Transition.Child>{() => <aside>Sidebar</aside>}</Transition.Child>
|
||||
<Transition.Child>{() => <section>Content</section>}</Transition.Child>
|
||||
</Dummy>
|
||||
)}
|
||||
</Transition>
|
||||
</div>
|
||||
@@ -503,6 +529,53 @@ describe('Transitions', () => {
|
||||
`)
|
||||
})
|
||||
|
||||
it('should transition in completely (duration defined in seconds) in (render strategy = hidden)', async () => {
|
||||
const enterDuration = 50
|
||||
|
||||
function Example() {
|
||||
const [show, setShow] = React.useState(false)
|
||||
|
||||
return (
|
||||
<>
|
||||
<style>{`.enter { transition-duration: ${enterDuration /
|
||||
1000}s; } .from { opacity: 0%; } .to { opacity: 100%; }`}</style>
|
||||
|
||||
<Transition show={show} unmount={false} enter="enter" enterFrom="from" enterTo="to">
|
||||
<span>Hello!</span>
|
||||
</Transition>
|
||||
|
||||
<button data-testid="toggle" onClick={() => setShow(v => !v)}>
|
||||
Toggle
|
||||
</button>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const timeline = await executeTimeline(<Example />, [
|
||||
// Toggle to show
|
||||
({ getByTestId }) => {
|
||||
fireEvent.click(getByTestId('toggle'))
|
||||
return executeTimeline.fullTransition(enterDuration)
|
||||
},
|
||||
])
|
||||
|
||||
expect(timeline).toMatchInlineSnapshot(`
|
||||
"Render 1:
|
||||
- hidden=\\"\\"
|
||||
- style=\\"display: none;\\"
|
||||
+ class=\\"enter from\\"
|
||||
+ style=\\"\\"
|
||||
|
||||
Render 2:
|
||||
- class=\\"enter from\\"
|
||||
+ class=\\"enter to\\"
|
||||
|
||||
Render 3: Transition took at least 50ms (yes)
|
||||
- class=\\"enter to\\"
|
||||
+ class=\\"\\""
|
||||
`)
|
||||
})
|
||||
|
||||
it('should transition in completely', async () => {
|
||||
const enterDuration = 50
|
||||
|
||||
@@ -606,6 +679,57 @@ describe('Transitions', () => {
|
||||
})
|
||||
)
|
||||
|
||||
it(
|
||||
'should transition out completely (render strategy = hidden)',
|
||||
suppressConsoleLogs(async () => {
|
||||
const leaveDuration = 50
|
||||
|
||||
function Example() {
|
||||
const [show, setShow] = React.useState(true)
|
||||
|
||||
return (
|
||||
<>
|
||||
<style>{`.leave { transition-duration: ${leaveDuration}ms; } .from { opacity: 0%; } .to { opacity: 100%; }`}</style>
|
||||
|
||||
<Transition show={show} unmount={false} leave="leave" leaveFrom="from" leaveTo="to">
|
||||
<span>Hello!</span>
|
||||
</Transition>
|
||||
|
||||
<button data-testid="toggle" onClick={() => setShow(v => !v)}>
|
||||
Toggle
|
||||
</button>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const timeline = await executeTimeline(<Example />, [
|
||||
// Toggle to hide
|
||||
({ getByTestId }) => {
|
||||
fireEvent.click(getByTestId('toggle'))
|
||||
return executeTimeline.fullTransition(leaveDuration)
|
||||
},
|
||||
])
|
||||
|
||||
expect(timeline).toMatchInlineSnapshot(`
|
||||
"Render 1:
|
||||
- <div>
|
||||
+ <div
|
||||
+ class=\\"leave from\\"
|
||||
+ >
|
||||
|
||||
Render 2:
|
||||
- class=\\"leave from\\"
|
||||
+ class=\\"leave to\\"
|
||||
|
||||
Render 3: Transition took at least 50ms (yes)
|
||||
- class=\\"leave to\\"
|
||||
+ class=\\"\\"
|
||||
+ hidden=\\"\\"
|
||||
+ style=\\"display: none;\\""
|
||||
`)
|
||||
})
|
||||
)
|
||||
|
||||
it(
|
||||
'should transition in and out completely',
|
||||
suppressConsoleLogs(async () => {
|
||||
@@ -690,6 +814,108 @@ describe('Transitions', () => {
|
||||
`)
|
||||
})
|
||||
)
|
||||
|
||||
it(
|
||||
'should transition in and out completely (render strategy = hidden)',
|
||||
suppressConsoleLogs(async () => {
|
||||
const enterDuration = 50
|
||||
const leaveDuration = 75
|
||||
|
||||
function Example() {
|
||||
const [show, setShow] = React.useState(false)
|
||||
|
||||
return (
|
||||
<>
|
||||
<style>{`.enter { transition-duration: ${enterDuration}ms; } .enter-from { opacity: 0%; } .enter-to { opacity: 100%; }`}</style>
|
||||
<style>{`.leave { transition-duration: ${leaveDuration}ms; } .leave-from { opacity: 100%; } .leave-to { opacity: 0%; }`}</style>
|
||||
|
||||
<Transition
|
||||
show={show}
|
||||
unmount={false}
|
||||
enter="enter"
|
||||
enterFrom="enter-from"
|
||||
enterTo="enter-to"
|
||||
leave="leave"
|
||||
leaveFrom="leave-from"
|
||||
leaveTo="leave-to"
|
||||
>
|
||||
<span>Hello!</span>
|
||||
</Transition>
|
||||
|
||||
<button data-testid="toggle" onClick={() => setShow(v => !v)}>
|
||||
Toggle
|
||||
</button>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const timeline = await executeTimeline(<Example />, [
|
||||
// Toggle to show
|
||||
({ getByTestId }) => {
|
||||
fireEvent.click(getByTestId('toggle'))
|
||||
return executeTimeline.fullTransition(enterDuration)
|
||||
},
|
||||
|
||||
// Toggle to hide
|
||||
({ getByTestId }) => {
|
||||
fireEvent.click(getByTestId('toggle'))
|
||||
return executeTimeline.fullTransition(leaveDuration)
|
||||
},
|
||||
|
||||
// Toggle to show
|
||||
({ getByTestId }) => {
|
||||
fireEvent.click(getByTestId('toggle'))
|
||||
return executeTimeline.fullTransition(leaveDuration)
|
||||
},
|
||||
])
|
||||
|
||||
expect(timeline).toMatchInlineSnapshot(`
|
||||
"Render 1:
|
||||
- hidden=\\"\\"
|
||||
- style=\\"display: none;\\"
|
||||
+ class=\\"enter enter-from\\"
|
||||
+ style=\\"\\"
|
||||
|
||||
Render 2:
|
||||
- class=\\"enter enter-from\\"
|
||||
+ class=\\"enter enter-to\\"
|
||||
|
||||
Render 3: Transition took at least 50ms (yes)
|
||||
- class=\\"enter enter-to\\"
|
||||
+ class=\\"\\"
|
||||
|
||||
Render 4:
|
||||
- class=\\"\\"
|
||||
+ class=\\"leave leave-from\\"
|
||||
|
||||
Render 5:
|
||||
- class=\\"leave leave-from\\"
|
||||
+ class=\\"leave leave-to\\"
|
||||
|
||||
Render 6: Transition took at least 75ms (yes)
|
||||
- class=\\"leave leave-to\\"
|
||||
- style=\\"\\"
|
||||
+ class=\\"\\"
|
||||
+ hidden=\\"\\"
|
||||
+ style=\\"display: none;\\"
|
||||
|
||||
Render 7:
|
||||
- class=\\"\\"
|
||||
- hidden=\\"\\"
|
||||
- style=\\"display: none;\\"
|
||||
+ class=\\"enter enter-from\\"
|
||||
+ style=\\"\\"
|
||||
|
||||
Render 8:
|
||||
- class=\\"enter enter-from\\"
|
||||
+ class=\\"enter enter-to\\"
|
||||
|
||||
Render 9: Transition took at least 75ms (yes)
|
||||
- class=\\"enter enter-to\\"
|
||||
+ class=\\"\\""
|
||||
`)
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
describe('nested transitions', () => {
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import * as React from 'react'
|
||||
import { Props } from 'types'
|
||||
|
||||
import { useId } from '../../hooks/use-id'
|
||||
import { useIsInitialRender } from '../../hooks/use-is-initial-render'
|
||||
import { match } from '../../utils/match'
|
||||
import { useIsMounted } from '../../hooks/use-is-mounted'
|
||||
import { useIsoMorphicEffect } from '../../hooks/use-iso-morphic-effect'
|
||||
|
||||
import { match } from '../../utils/match'
|
||||
import { Features, PropsForFeatures, render, RenderStrategy } from '../../utils/render'
|
||||
import { Reason, transition } from './utils/transition'
|
||||
|
||||
type ID = ReturnType<typeof useId>
|
||||
@@ -43,24 +45,9 @@ export type TransitionEvents = Partial<{
|
||||
afterLeave(): void
|
||||
}>
|
||||
|
||||
type HTMLTags = keyof JSX.IntrinsicElements
|
||||
type HTMLTagProps<TTag extends HTMLTags> = JSX.IntrinsicElements[TTag]
|
||||
|
||||
type AsShortcut<TTag extends HTMLTags> = {
|
||||
children?: React.ReactNode
|
||||
as?: TTag
|
||||
} & Omit<HTMLTagProps<TTag>, 'ref'>
|
||||
|
||||
type AsRenderPropFunction = {
|
||||
children: (ref: React.MutableRefObject<any>) => JSX.Element
|
||||
}
|
||||
|
||||
type BaseConfig = Partial<{ appear: boolean }>
|
||||
|
||||
type TransitionChildProps<TTag extends HTMLTags> = BaseConfig &
|
||||
(AsShortcut<TTag> | AsRenderPropFunction) &
|
||||
TransitionClasses &
|
||||
TransitionEvents
|
||||
type TransitionChildProps<TTag> = Props<TTag, TransitionChildRenderPropArg> &
|
||||
PropsForFeatures<typeof TransitionChildRenderFeatures> &
|
||||
Partial<{ appear: boolean } & TransitionClasses & TransitionEvents>
|
||||
|
||||
function useTransitionContext() {
|
||||
const context = React.useContext(TransitionContext)
|
||||
@@ -83,16 +70,23 @@ function useParentNesting() {
|
||||
}
|
||||
|
||||
type NestingContextValues = {
|
||||
children: React.MutableRefObject<ID[]>
|
||||
children: React.MutableRefObject<{ id: ID; state: TreeStates }[]>
|
||||
register: (id: ID) => () => void
|
||||
unregister: (id: ID) => void
|
||||
unregister: (id: ID, strategy?: RenderStrategy) => void
|
||||
}
|
||||
|
||||
const NestingContext = React.createContext<NestingContextValues | null>(null)
|
||||
|
||||
function hasChildren(
|
||||
bag: NestingContextValues['children'] | { children: NestingContextValues['children'] }
|
||||
): boolean {
|
||||
if ('children' in bag) return hasChildren(bag.children)
|
||||
return bag.current.filter(({ state }) => state === TreeStates.Visible).length > 0
|
||||
}
|
||||
|
||||
function useNesting(done?: () => void) {
|
||||
const doneRef = React.useRef(done)
|
||||
const transitionableChildren = React.useRef<ID[]>([])
|
||||
const transitionableChildren = React.useRef<NestingContextValues['children']['current']>([])
|
||||
const mounted = useIsMounted()
|
||||
|
||||
React.useEffect(() => {
|
||||
@@ -100,14 +94,20 @@ function useNesting(done?: () => void) {
|
||||
}, [done])
|
||||
|
||||
const unregister = React.useCallback(
|
||||
(childId: ID) => {
|
||||
const idx = transitionableChildren.current.indexOf(childId)
|
||||
|
||||
(childId: ID, strategy = RenderStrategy.Hidden) => {
|
||||
const idx = transitionableChildren.current.findIndex(({ id }) => id === childId)
|
||||
if (idx === -1) return
|
||||
|
||||
transitionableChildren.current.splice(idx, 1)
|
||||
match(strategy, {
|
||||
[RenderStrategy.Unmount]() {
|
||||
transitionableChildren.current.splice(idx, 1)
|
||||
},
|
||||
[RenderStrategy.Hidden]() {
|
||||
transitionableChildren.current[idx].state = TreeStates.Hidden
|
||||
},
|
||||
})
|
||||
|
||||
if (transitionableChildren.current.length <= 0 && mounted.current) {
|
||||
if (!hasChildren(transitionableChildren) && mounted.current) {
|
||||
doneRef.current?.()
|
||||
}
|
||||
},
|
||||
@@ -116,8 +116,14 @@ function useNesting(done?: () => void) {
|
||||
|
||||
const register = React.useCallback(
|
||||
(childId: ID) => {
|
||||
transitionableChildren.current.push(childId)
|
||||
return () => unregister(childId)
|
||||
const child = transitionableChildren.current.find(({ id }) => id === childId)
|
||||
if (!child) {
|
||||
transitionableChildren.current.push({ id: childId, state: TreeStates.Visible })
|
||||
} else if (child.state !== TreeStates.Visible) {
|
||||
child.state = TreeStates.Visible
|
||||
}
|
||||
|
||||
return () => unregister(childId, RenderStrategy.Unmount)
|
||||
},
|
||||
[transitionableChildren, unregister]
|
||||
)
|
||||
@@ -156,7 +162,15 @@ function useEvents(events: TransitionEvents) {
|
||||
return eventsRef
|
||||
}
|
||||
|
||||
function TransitionChild<TTag extends HTMLTags = 'div'>(props: TransitionChildProps<TTag>) {
|
||||
// ---
|
||||
|
||||
const DEFAULT_TRANSITION_CHILD_TAG = 'div'
|
||||
type TransitionChildRenderPropArg = React.MutableRefObject<HTMLDivElement>
|
||||
const TransitionChildRenderFeatures = Features.RenderStrategy
|
||||
|
||||
function TransitionChild<TTag extends React.ElementType = typeof DEFAULT_TRANSITION_CHILD_TAG>(
|
||||
props: TransitionChildProps<TTag>
|
||||
) {
|
||||
const {
|
||||
// Event "handlers"
|
||||
beforeEnter,
|
||||
@@ -171,13 +185,11 @@ function TransitionChild<TTag extends HTMLTags = 'div'>(props: TransitionChildPr
|
||||
leave,
|
||||
leaveFrom,
|
||||
leaveTo,
|
||||
|
||||
// ..
|
||||
children,
|
||||
...rest
|
||||
} = props
|
||||
const container = React.useRef<HTMLElement | null>(null)
|
||||
const [state, setState] = React.useState(TreeStates.Visible)
|
||||
const strategy = rest.unmount ? RenderStrategy.Unmount : RenderStrategy.Hidden
|
||||
|
||||
const { show, appear } = useTransitionContext()
|
||||
const { register, unregister } = useParentNesting()
|
||||
@@ -202,6 +214,23 @@ function TransitionChild<TTag extends HTMLTags = 'div'>(props: TransitionChildPr
|
||||
return register(id)
|
||||
}, [register, id])
|
||||
|
||||
useIsoMorphicEffect(() => {
|
||||
// If we are in another mode than the Hidden mode then ignore
|
||||
if (strategy !== RenderStrategy.Hidden) return
|
||||
if (!id) return
|
||||
|
||||
// Make sure that we are visible
|
||||
if (show && state !== TreeStates.Visible) {
|
||||
setState(TreeStates.Visible)
|
||||
return
|
||||
}
|
||||
|
||||
match(state, {
|
||||
[TreeStates.Hidden]: () => unregister(id),
|
||||
[TreeStates.Visible]: () => register(id),
|
||||
})
|
||||
}, [state, id, register, unregister, show, strategy])
|
||||
|
||||
const enterClasses = useSplitClasses(enter)
|
||||
const enterFromClasses = useSplitClasses(enterFrom)
|
||||
const enterToClasses = useSplitClasses(enterTo)
|
||||
@@ -243,7 +272,7 @@ function TransitionChild<TTag extends HTMLTags = 'div'>(props: TransitionChildPr
|
||||
|
||||
// When we don't have children anymore we can safely unregister from the parent and hide
|
||||
// ourselves.
|
||||
if (nesting.children.current.length <= 0) {
|
||||
if (!hasChildren(nesting)) {
|
||||
setState(TreeStates.Hidden)
|
||||
unregister(id)
|
||||
events.current.afterLeave()
|
||||
@@ -266,32 +295,27 @@ function TransitionChild<TTag extends HTMLTags = 'div'>(props: TransitionChildPr
|
||||
leaveToClasses,
|
||||
])
|
||||
|
||||
// Unmount the whole tree
|
||||
if (state === TreeStates.Hidden) return null
|
||||
const propsBag = {}
|
||||
const propsWeControl = { ref: container }
|
||||
const passthroughProps = rest
|
||||
|
||||
if (typeof children === 'function') {
|
||||
return (
|
||||
<NestingContext.Provider value={nesting}>
|
||||
{(children as AsRenderPropFunction['children'])(container)}
|
||||
</NestingContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
const { as: Component = 'div', ...passthroughProps } = rest as AsShortcut<TTag>
|
||||
return (
|
||||
<NestingContext.Provider value={nesting}>
|
||||
{/* @ts-expect-error Expression produces a union type that is too complex to represent. */}
|
||||
<Component {...passthroughProps} ref={container}>
|
||||
{children}
|
||||
</Component>
|
||||
{render(
|
||||
{ ...passthroughProps, ...propsWeControl },
|
||||
propsBag,
|
||||
DEFAULT_TRANSITION_CHILD_TAG,
|
||||
TransitionChildRenderFeatures,
|
||||
state === TreeStates.Visible
|
||||
)}
|
||||
</NestingContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export function Transition<TTag extends HTMLTags = 'div'>(
|
||||
export function Transition<TTag extends React.ElementType = typeof DEFAULT_TRANSITION_CHILD_TAG>(
|
||||
props: TransitionChildProps<TTag> & { show: boolean; appear?: boolean }
|
||||
) {
|
||||
const { show, appear = false, ...rest } = props
|
||||
const { show, appear = false, unmount, ...passthroughProps } = props
|
||||
|
||||
if (![true, false].includes(show)) {
|
||||
throw new Error('A <Transition /> is used but it is missing a `show={true | false}` prop.')
|
||||
@@ -312,18 +336,28 @@ export function Transition<TTag extends HTMLTags = 'div'>(
|
||||
React.useEffect(() => {
|
||||
if (show) {
|
||||
setState(TreeStates.Visible)
|
||||
} else if (nestingBag.children.current.length <= 0) {
|
||||
} else if (!hasChildren(nestingBag)) {
|
||||
setState(TreeStates.Hidden)
|
||||
}
|
||||
}, [show, nestingBag])
|
||||
|
||||
const sharedProps = { unmount }
|
||||
const propsBag = {}
|
||||
|
||||
return (
|
||||
<NestingContext.Provider value={nestingBag}>
|
||||
<TransitionContext.Provider value={transitionBag}>
|
||||
{match(state, {
|
||||
[TreeStates.Visible]: () => <TransitionChild {...rest} />,
|
||||
[TreeStates.Hidden]: null,
|
||||
})}
|
||||
{render(
|
||||
{
|
||||
...sharedProps,
|
||||
as: React.Fragment,
|
||||
children: <TransitionChild {...sharedProps} {...passthroughProps} />,
|
||||
},
|
||||
propsBag,
|
||||
React.Fragment,
|
||||
TransitionChildRenderFeatures,
|
||||
state === TreeStates.Visible
|
||||
)}
|
||||
</TransitionContext.Provider>
|
||||
</NestingContext.Provider>
|
||||
)
|
||||
|
||||
@@ -27,8 +27,14 @@ export function getMenuItems(): HTMLElement[] {
|
||||
// ---
|
||||
|
||||
export enum MenuState {
|
||||
Open,
|
||||
Closed,
|
||||
/** The menu is visible to the user. */
|
||||
Visible,
|
||||
|
||||
/** The menu is **not** visible to the user. It's still in the DOM, but it is hidden. */
|
||||
InvisibleHidden,
|
||||
|
||||
/** The menu is **not** visible to the user. It's not in the DOM, it is unmounted. */
|
||||
InvisibleUnmounted,
|
||||
}
|
||||
|
||||
export function assertMenuButton(
|
||||
@@ -47,12 +53,17 @@ export function assertMenuButton(
|
||||
expect(button).toHaveAttribute('aria-haspopup')
|
||||
|
||||
switch (options.state) {
|
||||
case MenuState.Open:
|
||||
case MenuState.Visible:
|
||||
expect(button).toHaveAttribute('aria-controls')
|
||||
expect(button).toHaveAttribute('aria-expanded', 'true')
|
||||
break
|
||||
|
||||
case MenuState.Closed:
|
||||
case MenuState.InvisibleHidden:
|
||||
expect(button).toHaveAttribute('aria-controls')
|
||||
expect(button).not.toHaveAttribute('aria-expanded')
|
||||
break
|
||||
|
||||
case MenuState.InvisibleUnmounted:
|
||||
expect(button).not.toHaveAttribute('aria-controls')
|
||||
expect(button).not.toHaveAttribute('aria-expanded')
|
||||
break
|
||||
@@ -124,27 +135,37 @@ export function assertMenu(
|
||||
) {
|
||||
try {
|
||||
switch (options.state) {
|
||||
case MenuState.Open:
|
||||
case MenuState.InvisibleHidden:
|
||||
if (menu === null) return expect(menu).not.toBe(null)
|
||||
|
||||
// Check that some attributes exists, doesn't really matter what the values are at this point in
|
||||
// time, we just require them.
|
||||
expect(menu).toHaveAttribute('aria-labelledby')
|
||||
assertHidden(menu)
|
||||
|
||||
// Check that we have the correct values for certain attributes
|
||||
expect(menu).toHaveAttribute('aria-labelledby')
|
||||
expect(menu).toHaveAttribute('role', 'menu')
|
||||
|
||||
if (options.textContent) {
|
||||
expect(menu).toHaveTextContent(options.textContent)
|
||||
}
|
||||
if (options.textContent) expect(menu).toHaveTextContent(options.textContent)
|
||||
|
||||
// Ensure menu button has the following attributes
|
||||
for (let attributeName in options.attributes) {
|
||||
expect(menu).toHaveAttribute(attributeName, options.attributes[attributeName])
|
||||
}
|
||||
break
|
||||
|
||||
case MenuState.Closed:
|
||||
case MenuState.Visible:
|
||||
if (menu === null) return expect(menu).not.toBe(null)
|
||||
|
||||
assertVisible(menu)
|
||||
|
||||
expect(menu).toHaveAttribute('aria-labelledby')
|
||||
expect(menu).toHaveAttribute('role', 'menu')
|
||||
|
||||
if (options.textContent) expect(menu).toHaveTextContent(options.textContent)
|
||||
|
||||
for (let attributeName in options.attributes) {
|
||||
expect(menu).toHaveAttribute(attributeName, options.attributes[attributeName])
|
||||
}
|
||||
break
|
||||
|
||||
case MenuState.InvisibleUnmounted:
|
||||
expect(menu).toBe(null)
|
||||
break
|
||||
|
||||
@@ -217,8 +238,14 @@ export function getListboxOptions(): HTMLElement[] {
|
||||
// ---
|
||||
|
||||
export enum ListboxState {
|
||||
Open,
|
||||
Closed,
|
||||
/** The listbox is visible to the user. */
|
||||
Visible,
|
||||
|
||||
/** The listbox is **not** visible to the user. It's still in the DOM, but it is hidden. */
|
||||
InvisibleHidden,
|
||||
|
||||
/** The listbox is **not** visible to the user. It's not in the DOM, it is unmounted. */
|
||||
InvisibleUnmounted,
|
||||
}
|
||||
|
||||
export function assertListbox(
|
||||
@@ -231,27 +258,37 @@ export function assertListbox(
|
||||
) {
|
||||
try {
|
||||
switch (options.state) {
|
||||
case ListboxState.Open:
|
||||
case ListboxState.InvisibleHidden:
|
||||
if (listbox === null) return expect(listbox).not.toBe(null)
|
||||
|
||||
// Check that some attributes exists, doesn't really matter what the values are at this point in
|
||||
// time, we just require them.
|
||||
expect(listbox).toHaveAttribute('aria-labelledby')
|
||||
assertHidden(listbox)
|
||||
|
||||
// Check that we have the correct values for certain attributes
|
||||
expect(listbox).toHaveAttribute('aria-labelledby')
|
||||
expect(listbox).toHaveAttribute('role', 'listbox')
|
||||
|
||||
if (options.textContent) {
|
||||
expect(listbox).toHaveTextContent(options.textContent)
|
||||
}
|
||||
if (options.textContent) expect(listbox).toHaveTextContent(options.textContent)
|
||||
|
||||
// Ensure listbox button has the following attributes
|
||||
for (let attributeName in options.attributes) {
|
||||
expect(listbox).toHaveAttribute(attributeName, options.attributes[attributeName])
|
||||
}
|
||||
break
|
||||
|
||||
case ListboxState.Closed:
|
||||
case ListboxState.Visible:
|
||||
if (listbox === null) return expect(listbox).not.toBe(null)
|
||||
|
||||
assertVisible(listbox)
|
||||
|
||||
expect(listbox).toHaveAttribute('aria-labelledby')
|
||||
expect(listbox).toHaveAttribute('role', 'listbox')
|
||||
|
||||
if (options.textContent) expect(listbox).toHaveTextContent(options.textContent)
|
||||
|
||||
for (let attributeName in options.attributes) {
|
||||
expect(listbox).toHaveAttribute(attributeName, options.attributes[attributeName])
|
||||
}
|
||||
break
|
||||
|
||||
case ListboxState.InvisibleUnmounted:
|
||||
expect(listbox).toBe(null)
|
||||
break
|
||||
|
||||
@@ -280,12 +317,17 @@ export function assertListboxButton(
|
||||
expect(button).toHaveAttribute('aria-haspopup')
|
||||
|
||||
switch (options.state) {
|
||||
case ListboxState.Open:
|
||||
case ListboxState.Visible:
|
||||
expect(button).toHaveAttribute('aria-controls')
|
||||
expect(button).toHaveAttribute('aria-expanded', 'true')
|
||||
break
|
||||
|
||||
case ListboxState.Closed:
|
||||
case ListboxState.InvisibleHidden:
|
||||
expect(button).toHaveAttribute('aria-controls')
|
||||
expect(button).not.toHaveAttribute('aria-expanded')
|
||||
break
|
||||
|
||||
case ListboxState.InvisibleUnmounted:
|
||||
expect(button).not.toHaveAttribute('aria-controls')
|
||||
expect(button).not.toHaveAttribute('aria-expanded')
|
||||
break
|
||||
@@ -567,3 +609,29 @@ export function assertActiveElement(element: HTMLElement | null) {
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
// ---
|
||||
|
||||
export function assertHidden(element: HTMLElement | null) {
|
||||
try {
|
||||
if (element === null) return expect(element).not.toBe(null)
|
||||
|
||||
expect(element).toHaveAttribute('hidden')
|
||||
expect(element).toHaveStyle({ display: 'none' })
|
||||
} catch (err) {
|
||||
Error.captureStackTrace(err, assertHidden)
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
export function assertVisible(element: HTMLElement | null) {
|
||||
try {
|
||||
if (element === null) return expect(element).not.toBe(null)
|
||||
|
||||
expect(element).not.toHaveAttribute('hidden')
|
||||
expect(element).not.toHaveStyle({ display: 'none' })
|
||||
} catch (err) {
|
||||
Error.captureStackTrace(err, assertVisible)
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,25 @@
|
||||
// A unique placeholder we can use as some defaults. This is nice because we can use this instead of
|
||||
// defaulting to null / never / ... and possibly collide with actual data.
|
||||
const __: unique symbol = Symbol('__placeholder__')
|
||||
export type __ = typeof __
|
||||
|
||||
export type PropsOf<TTag = any> = TTag extends React.ElementType
|
||||
? React.ComponentProps<TTag>
|
||||
: never
|
||||
|
||||
export type Props<TTag, TSlot = {}, TOmitableProps extends keyof any = ''> = {
|
||||
export type Props<TTag, TSlot = {}, TOmitableProps extends keyof any = __> = {
|
||||
as?: TTag
|
||||
children?: React.ReactNode | ((bag: TSlot) => React.ReactElement)
|
||||
} & Omit<PropsOf<TTag>, TOmitableProps>
|
||||
refName?: string
|
||||
} & (TOmitableProps extends __ ? PropsOf<TTag> : Omit<PropsOf<TTag>, TOmitableProps>)
|
||||
|
||||
type Without<T, U> = { [P in Exclude<keyof T, keyof U>]?: never }
|
||||
export type XOR<T, U> = T | U extends __
|
||||
? never
|
||||
: T extends __
|
||||
? U
|
||||
: U extends __
|
||||
? T
|
||||
: T | U extends object
|
||||
? (Without<T, U> & U) | (Without<U, T> & T)
|
||||
: T | U
|
||||
|
||||
@@ -0,0 +1,406 @@
|
||||
import React from 'react'
|
||||
import { render as testRender, prettyDOM, getByTestId } from '@testing-library/react'
|
||||
|
||||
import { suppressConsoleLogs } from '../test-utils/suppress-console-logs'
|
||||
import { render, Features, PropsForFeatures } from './render'
|
||||
import { Props } from '../types'
|
||||
|
||||
function contents() {
|
||||
return prettyDOM(getByTestId(document.body, 'wrapper'), undefined, {
|
||||
highlight: false,
|
||||
})
|
||||
}
|
||||
|
||||
describe('Default functionality', () => {
|
||||
const bag = {}
|
||||
function Dummy<TTag extends React.ElementType = 'div'>(
|
||||
props: Props<TTag> & Partial<{ a: any; b: any; c: any }>
|
||||
) {
|
||||
return <div data-testid="wrapper">{render(props, bag, 'div')}</div>
|
||||
}
|
||||
|
||||
it('should be possible to render a dummy component', () => {
|
||||
testRender(<Dummy />)
|
||||
|
||||
expect(contents()).toMatchInlineSnapshot(`
|
||||
"<div
|
||||
data-testid=\\"wrapper\\"
|
||||
>
|
||||
<div />
|
||||
</div>"
|
||||
`)
|
||||
})
|
||||
|
||||
it('should be possible to render a dummy component with some children as a callback', () => {
|
||||
expect.assertions(2)
|
||||
|
||||
testRender(
|
||||
<Dummy>
|
||||
{data => {
|
||||
expect(data).toBe(bag)
|
||||
|
||||
return <span>Contents</span>
|
||||
}}
|
||||
</Dummy>
|
||||
)
|
||||
|
||||
expect(contents()).toMatchInlineSnapshot(`
|
||||
"<div
|
||||
data-testid=\\"wrapper\\"
|
||||
>
|
||||
<div>
|
||||
<span>
|
||||
Contents
|
||||
</span>
|
||||
</div>
|
||||
</div>"
|
||||
`)
|
||||
})
|
||||
|
||||
it('should be possible to add a ref with a different name', () => {
|
||||
const ref = React.createRef()
|
||||
|
||||
function MyComponent<T extends React.ElementType = 'div'>({
|
||||
innerRef,
|
||||
...props
|
||||
}: Props<T> & { innerRef: React.Ref<HTMLDivElement> }) {
|
||||
return <div ref={innerRef} {...props} />
|
||||
}
|
||||
|
||||
function OtherDummy<TTag extends React.ElementType = 'div'>(props: Props<TTag>) {
|
||||
return <div data-testid="wrapper">{render({ ...props, ref }, bag, 'div')}</div>
|
||||
}
|
||||
|
||||
testRender(
|
||||
<OtherDummy as={MyComponent} refName="potato">
|
||||
Contents
|
||||
</OtherDummy>
|
||||
)
|
||||
|
||||
expect(contents()).toMatchInlineSnapshot(`
|
||||
"<div
|
||||
data-testid=\\"wrapper\\"
|
||||
>
|
||||
<div
|
||||
potato=\\"[object Object]\\"
|
||||
>
|
||||
Contents
|
||||
</div>
|
||||
</div>"
|
||||
`)
|
||||
})
|
||||
|
||||
it('should be possible to passthrough props to a dummy component', () => {
|
||||
testRender(<Dummy a={1} b={2} c={3} />)
|
||||
|
||||
expect(contents()).toMatchInlineSnapshot(`
|
||||
"<div
|
||||
data-testid=\\"wrapper\\"
|
||||
>
|
||||
<div
|
||||
a=\\"1\\"
|
||||
b=\\"2\\"
|
||||
c=\\"3\\"
|
||||
/>
|
||||
</div>"
|
||||
`)
|
||||
})
|
||||
|
||||
it('should be possible to change the underlying DOM node using the `as` prop', () => {
|
||||
testRender(<Dummy as="button" />)
|
||||
|
||||
expect(contents()).toMatchInlineSnapshot(`
|
||||
"<div
|
||||
data-testid=\\"wrapper\\"
|
||||
>
|
||||
<button />
|
||||
</div>"
|
||||
`)
|
||||
})
|
||||
|
||||
it('should be possible to change the underlying DOM node using the `as` prop and still have a function as children', () => {
|
||||
testRender(<Dummy as="button">{() => <span>Contents</span>}</Dummy>)
|
||||
|
||||
expect(contents()).toMatchInlineSnapshot(`
|
||||
"<div
|
||||
data-testid=\\"wrapper\\"
|
||||
>
|
||||
<button>
|
||||
<span>
|
||||
Contents
|
||||
</span>
|
||||
</button>
|
||||
</div>"
|
||||
`)
|
||||
})
|
||||
|
||||
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>)
|
||||
|
||||
expect(contents()).toMatchInlineSnapshot(`
|
||||
"<div
|
||||
data-testid=\\"wrapper\\"
|
||||
>
|
||||
Contents
|
||||
</div>"
|
||||
`)
|
||||
})
|
||||
|
||||
it('should forward all the props to the first child when using an as={React.Fragment}', () => {
|
||||
testRender(
|
||||
<Dummy as={React.Fragment} a={1} b={1}>
|
||||
{() => <span>Contents</span>}
|
||||
</Dummy>
|
||||
)
|
||||
|
||||
expect(contents()).toMatchInlineSnapshot(`
|
||||
"<div
|
||||
data-testid=\\"wrapper\\"
|
||||
>
|
||||
<span
|
||||
a=\\"1\\"
|
||||
b=\\"1\\"
|
||||
>
|
||||
Contents
|
||||
</span>
|
||||
</div>"
|
||||
`)
|
||||
})
|
||||
|
||||
it(
|
||||
'should error when we are rendering a React.Fragment with multiple children',
|
||||
suppressConsoleLogs(() => {
|
||||
expect.assertions(1)
|
||||
|
||||
return expect(() => {
|
||||
testRender(
|
||||
// @ts-expect-error className cannot be applied to a React.Fragment
|
||||
<Dummy as={React.Fragment} className="p-12">
|
||||
<span>Contents A</span>
|
||||
<span>Contents B</span>
|
||||
</Dummy>
|
||||
)
|
||||
}).toThrowErrorMatchingInlineSnapshot(`"You should only render 1 child"`)
|
||||
})
|
||||
)
|
||||
|
||||
it("should not error when we are rendering a React.Fragment with multiple children when we don't passthrough additional props", () => {
|
||||
testRender(
|
||||
<Dummy as={React.Fragment}>
|
||||
<span>Contents A</span>
|
||||
<span>Contents B</span>
|
||||
</Dummy>
|
||||
)
|
||||
|
||||
expect(contents()).toMatchInlineSnapshot(`
|
||||
"<div
|
||||
data-testid=\\"wrapper\\"
|
||||
>
|
||||
<span>
|
||||
Contents A
|
||||
</span>
|
||||
<span>
|
||||
Contents B
|
||||
</span>
|
||||
</div>"
|
||||
`)
|
||||
})
|
||||
|
||||
it(
|
||||
'should error when we are applying props to a React.Fragment when we do not have a dedicated element',
|
||||
suppressConsoleLogs(() => {
|
||||
expect.assertions(1)
|
||||
|
||||
return expect(() => {
|
||||
testRender(
|
||||
// @ts-expect-error className cannot be applied to a React.Fragment
|
||||
<Dummy as={React.Fragment} className="p-12">
|
||||
Contents
|
||||
</Dummy>
|
||||
)
|
||||
}).toThrowErrorMatchingInlineSnapshot(
|
||||
`"You should render an element as a child. Did you forget the as=\\"...\\" prop?"`
|
||||
)
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
// ---
|
||||
|
||||
function testStaticFeature(Dummy) {
|
||||
it('should be possible to render a `static` dummy component (show = true)', () => {
|
||||
testRender(
|
||||
<Dummy show={true} static>
|
||||
Contents
|
||||
</Dummy>
|
||||
)
|
||||
|
||||
expect(contents()).toMatchInlineSnapshot(`
|
||||
"<div
|
||||
data-testid=\\"wrapper\\"
|
||||
>
|
||||
<div>
|
||||
Contents
|
||||
</div>
|
||||
</div>"
|
||||
`)
|
||||
})
|
||||
|
||||
it('should be possible to render a `static` dummy component (show = false)', () => {
|
||||
testRender(
|
||||
<Dummy show={false} static>
|
||||
Contents
|
||||
</Dummy>
|
||||
)
|
||||
|
||||
expect(contents()).toMatchInlineSnapshot(`
|
||||
"<div
|
||||
data-testid=\\"wrapper\\"
|
||||
>
|
||||
<div>
|
||||
Contents
|
||||
</div>
|
||||
</div>"
|
||||
`)
|
||||
})
|
||||
}
|
||||
|
||||
// With the `static` keyword, the user is always in control. When we internally decide to show the
|
||||
// component or hide it then it won't have any effect. This is useful for when you want to wrap your
|
||||
// component in a Transition for example so that the Transition component can control the
|
||||
// showing/hiding based on the `show` prop AND the state of the transition.
|
||||
describe('Features.Static', () => {
|
||||
const bag = {}
|
||||
const EnabledFeatures = Features.Static
|
||||
function Dummy<TTag extends React.ElementType = 'div'>(
|
||||
props: Props<TTag> & { show: boolean } & PropsForFeatures<typeof EnabledFeatures>
|
||||
) {
|
||||
const { show, ...rest } = props
|
||||
return <div data-testid="wrapper">{render(rest, bag, 'div', EnabledFeatures, show)}</div>
|
||||
}
|
||||
|
||||
testStaticFeature(Dummy)
|
||||
})
|
||||
|
||||
// ---
|
||||
|
||||
function testRenderStrategyFeature(Dummy) {
|
||||
describe('Unmount render strategy', () => {
|
||||
it('should be possible to render an `unmount` dummy component (show = true)', () => {
|
||||
testRender(
|
||||
<Dummy show={true} unmount>
|
||||
Contents
|
||||
</Dummy>
|
||||
)
|
||||
|
||||
expect(contents()).toMatchInlineSnapshot(`
|
||||
"<div
|
||||
data-testid=\\"wrapper\\"
|
||||
>
|
||||
<div>
|
||||
Contents
|
||||
</div>
|
||||
</div>"
|
||||
`)
|
||||
})
|
||||
|
||||
it('should be possible to render an `unmount` dummy component (show = false)', () => {
|
||||
testRender(
|
||||
<Dummy show={false} unmount>
|
||||
Contents
|
||||
</Dummy>
|
||||
)
|
||||
|
||||
// No contents, because we unmounted!
|
||||
expect(contents()).toMatchInlineSnapshot(`
|
||||
"<div
|
||||
data-testid=\\"wrapper\\"
|
||||
/>"
|
||||
`)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Hidden render strategy', () => {
|
||||
it('should be possible to render an `unmount={false}` dummy component (show = true)', () => {
|
||||
testRender(
|
||||
<Dummy show={true} unmount={false}>
|
||||
Contents
|
||||
</Dummy>
|
||||
)
|
||||
|
||||
expect(contents()).toMatchInlineSnapshot(`
|
||||
"<div
|
||||
data-testid=\\"wrapper\\"
|
||||
>
|
||||
<div>
|
||||
Contents
|
||||
</div>
|
||||
</div>"
|
||||
`)
|
||||
})
|
||||
|
||||
it('should be possible to render an `unmount={false}` dummy component (show = false)', () => {
|
||||
testRender(
|
||||
<Dummy show={false} unmount={false}>
|
||||
Contents
|
||||
</Dummy>
|
||||
)
|
||||
|
||||
// We do have contents, but it is marked as hidden!
|
||||
expect(contents()).toMatchInlineSnapshot(`
|
||||
"<div
|
||||
data-testid=\\"wrapper\\"
|
||||
>
|
||||
<div
|
||||
hidden=\\"\\"
|
||||
style=\\"display: none;\\"
|
||||
>
|
||||
Contents
|
||||
</div>
|
||||
</div>"
|
||||
`)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
describe('Features.RenderStrategy', () => {
|
||||
const bag = {}
|
||||
const EnabledFeatures = Features.RenderStrategy
|
||||
function Dummy<TTag extends React.ElementType = 'div'>(
|
||||
props: Props<TTag> & { show: boolean } & PropsForFeatures<typeof EnabledFeatures>
|
||||
) {
|
||||
const { show, ...rest } = props
|
||||
return <div data-testid="wrapper">{render(rest, bag, 'div', EnabledFeatures, show)}</div>
|
||||
}
|
||||
|
||||
testRenderStrategyFeature(Dummy)
|
||||
})
|
||||
|
||||
// ---
|
||||
|
||||
// This should enable the `static` and `unmount` features. However they can't be used together!
|
||||
describe('Features.Static | Features.RenderStrategy', () => {
|
||||
const bag = {}
|
||||
const EnabledFeatures = Features.Static | Features.RenderStrategy
|
||||
function Dummy<TTag extends React.ElementType = 'div'>(
|
||||
props: Props<TTag> & { show: boolean } & PropsForFeatures<typeof EnabledFeatures>
|
||||
) {
|
||||
const { show, ...rest } = props
|
||||
return <div data-testid="wrapper">{render(rest, bag, 'div', EnabledFeatures, show)}</div>
|
||||
}
|
||||
|
||||
// TODO: Can we "legit" test this? 🤔
|
||||
it('should result in a typescript error', () => {
|
||||
testRender(
|
||||
// @ts-expect-error static & unmount together are incompatible
|
||||
<Dummy show={false} static unmount>
|
||||
Contents
|
||||
</Dummy>
|
||||
)
|
||||
})
|
||||
|
||||
// To avoid duplication, and to make sure that the features tested in isolation can also be
|
||||
// re-used when they are combined.
|
||||
testStaticFeature(Dummy)
|
||||
testRenderStrategyFeature(Dummy)
|
||||
})
|
||||
@@ -1,12 +1,94 @@
|
||||
import * as React from 'react'
|
||||
import { Props } from '../types'
|
||||
import { Props, XOR, __ } from '../types'
|
||||
import { match } from './match'
|
||||
|
||||
export function render<TTag extends React.ElementType, TBag>(
|
||||
props: Props<TTag, TBag, any>,
|
||||
export enum Features {
|
||||
/** No features at all */
|
||||
None = 0,
|
||||
|
||||
/**
|
||||
* When used, this will allow us to use one of the render strategies.
|
||||
*
|
||||
* **The render strategies are:**
|
||||
* - **Unmount** _(Will unmount the component.)_
|
||||
* - **Hidden** _(Will hide the component using the [hidden] attribute.)_
|
||||
*/
|
||||
RenderStrategy = 1,
|
||||
|
||||
/**
|
||||
* When used, this will allow the user of our component to be in control. This can be used when
|
||||
* you want to transition based on some state.
|
||||
*/
|
||||
Static = 2,
|
||||
}
|
||||
|
||||
export enum RenderStrategy {
|
||||
Unmount,
|
||||
Hidden,
|
||||
}
|
||||
|
||||
type PropsForFeature<TPassedInFeatures extends Features, TForFeature extends Features, TProps> = {
|
||||
[P in TPassedInFeatures]: P extends TForFeature ? TProps : __
|
||||
}[TPassedInFeatures]
|
||||
|
||||
export type PropsForFeatures<T extends Features> = XOR<
|
||||
PropsForFeature<T, Features.Static, { static?: boolean }>,
|
||||
PropsForFeature<T, Features.RenderStrategy, { unmount?: boolean }>
|
||||
>
|
||||
|
||||
export function render<TFeature extends Features, TTag extends React.ElementType, TBag>(
|
||||
props: Props<TTag, TBag, any> & PropsForFeatures<TFeature>,
|
||||
propsBag: TBag,
|
||||
defaultTag: React.ElementType,
|
||||
features?: TFeature,
|
||||
visible: boolean = true
|
||||
) {
|
||||
// Visible always render
|
||||
if (visible) return _render(props, propsBag, defaultTag)
|
||||
|
||||
const featureFlags = features ?? Features.None
|
||||
|
||||
if (featureFlags & Features.Static) {
|
||||
const { static: isStatic = false, ...rest } = props as PropsForFeatures<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
|
||||
|
||||
return match(strategy, {
|
||||
[RenderStrategy.Unmount]() {
|
||||
return null
|
||||
},
|
||||
[RenderStrategy.Hidden]() {
|
||||
return _render(
|
||||
{ ...rest, ...{ hidden: true, style: { display: 'none' } } },
|
||||
propsBag,
|
||||
defaultTag
|
||||
)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// No features enabled, just render
|
||||
return _render(props, propsBag, defaultTag)
|
||||
}
|
||||
|
||||
function _render<TTag extends React.ElementType, TBag>(
|
||||
props: Props<TTag, TBag> & { ref?: unknown },
|
||||
bag: TBag,
|
||||
tag: React.ElementType
|
||||
) {
|
||||
const { as: Component = tag, children, ...passThroughProps } = props
|
||||
const { as: Component = tag, children, refName = 'ref', ...passThroughProps } = omit(props, [
|
||||
'unmount',
|
||||
'static',
|
||||
])
|
||||
|
||||
// This allows us to use `<HeadlessUIComponent as={MyComopnent} refName="innerRef" />`
|
||||
const refRelatedProps = props.ref !== undefined ? { [refName]: props.ref } : {}
|
||||
|
||||
const resolvedChildren = (typeof children === 'function' ? children(bag) : children) as
|
||||
| React.ReactElement
|
||||
@@ -16,7 +98,7 @@ export function render<TTag extends React.ElementType, TBag>(
|
||||
if (Object.keys(passThroughProps).length > 0) {
|
||||
if (Array.isArray(resolvedChildren) && resolvedChildren.length > 1) {
|
||||
const err = new Error('You should only render 1 child')
|
||||
if (Error.captureStackTrace) Error.captureStackTrace(err, render)
|
||||
if (Error.captureStackTrace) Error.captureStackTrace(err, _render)
|
||||
throw err
|
||||
}
|
||||
|
||||
@@ -24,20 +106,33 @@ export function render<TTag extends React.ElementType, TBag>(
|
||||
const err = new Error(
|
||||
`You should render an element as a child. Did you forget the as="..." prop?`
|
||||
)
|
||||
if (Error.captureStackTrace) Error.captureStackTrace(err, render)
|
||||
if (Error.captureStackTrace) Error.captureStackTrace(err, _render)
|
||||
throw err
|
||||
}
|
||||
|
||||
return React.cloneElement(
|
||||
resolvedChildren,
|
||||
|
||||
// Filter out undefined values so that they don't override the existing values
|
||||
mergeEventFunctions(compact(passThroughProps), resolvedChildren.props, ['onClick'])
|
||||
Object.assign(
|
||||
{},
|
||||
// Filter out undefined values so that they don't override the existing values
|
||||
mergeEventFunctions(compact(omit(passThroughProps, ['ref'])), resolvedChildren.props, [
|
||||
'onClick',
|
||||
]),
|
||||
refRelatedProps
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return React.createElement(Component, passThroughProps, resolvedChildren)
|
||||
return React.createElement(
|
||||
Component,
|
||||
Object.assign(
|
||||
{},
|
||||
omit(passThroughProps, ['ref']),
|
||||
Component !== React.Fragment && refRelatedProps
|
||||
),
|
||||
resolvedChildren
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -92,3 +187,11 @@ function compact<T extends Record<any, any>>(object: T) {
|
||||
}
|
||||
return clone
|
||||
}
|
||||
|
||||
function omit<T extends Record<any, any>>(object: T, keysToOmit: string[] = []) {
|
||||
let clone = Object.assign({}, object)
|
||||
for (let key of keysToOmit) {
|
||||
if (key in clone) delete clone[key]
|
||||
}
|
||||
return clone
|
||||
}
|
||||
|
||||
@@ -338,10 +338,11 @@ To tell an element to render its children directly with no wrapper element, use
|
||||
|
||||
##### Props
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| -------- | ------------------- | ------- | --------------------------------------------------------------------------- |
|
||||
| `as` | String \| Component | `div` | The element or component the `MenuItems` should render as. |
|
||||
| `static` | Boolean | `false` | Whether the element should ignore the internally managed open/closed state. |
|
||||
| Prop | Type | Default | Description |
|
||||
| --------- | ------------------- | ------- | --------------------------------------------------------------------------------- |
|
||||
| `as` | String \| Component | `div` | The element or component the `MenuItems` should render as. |
|
||||
| `static` | Boolean | `false` | Whether the element should ignore the internally managed open/closed state. |
|
||||
| `unmount` | Boolean | `true` | Whether the element should be unmounted or hidden based on the open/closed state. |
|
||||
|
||||
##### Slot props
|
||||
|
||||
@@ -1029,10 +1030,11 @@ export default {
|
||||
|
||||
##### Props
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| -------- | ------------------- | ------- | --------------------------------------------------------------------------- |
|
||||
| `as` | String \| Component | `ul` | The element or component the `ListboxOptions` should render as. |
|
||||
| `static` | Boolean | `false` | Whether the element should ignore the internally managed open/closed state. |
|
||||
| Prop | Type | Default | Description |
|
||||
| --------- | ------------------- | ------- | --------------------------------------------------------------------------------- |
|
||||
| `as` | String \| Component | `ul` | The element or component the `ListboxOptions` should render as. |
|
||||
| `static` | Boolean | `false` | Whether the element should ignore the internally managed open/closed state. |
|
||||
| `unmount` | Boolean | `true` | Whether the element should be unmounted or hidden based on the open/closed state. |
|
||||
|
||||
##### Slot props
|
||||
|
||||
|
||||
@@ -34,8 +34,8 @@
|
||||
"@popperjs/core": "^2.5.3",
|
||||
"@testing-library/vue": "^5.1.0",
|
||||
"@types/debounce": "^1.2.0",
|
||||
"@vue/compiler-sfc": "3.0.0",
|
||||
"@vue/test-utils": "^2.0.0-beta.6",
|
||||
"@vue/compiler-sfc": "3.0.1",
|
||||
"@vue/test-utils": "^2.0.0-beta.7",
|
||||
"husky": "^4.3.0",
|
||||
"vite": "^1.0.0-rc.4",
|
||||
"vue": "^3.0.0-rc.13",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -12,9 +12,10 @@ import {
|
||||
ComputedRef,
|
||||
watchEffect,
|
||||
toRaw,
|
||||
watch,
|
||||
} from 'vue'
|
||||
import { match } from '../../utils/match'
|
||||
import { render } from '../../utils/render'
|
||||
import { Features, render } from '../../utils/render'
|
||||
import { useId } from '../../hooks/use-id'
|
||||
import { Keys } from '../../keyboard'
|
||||
|
||||
@@ -142,7 +143,10 @@ export const Listbox = defineComponent({
|
||||
options,
|
||||
searchQuery,
|
||||
activeOptionIndex,
|
||||
closeListbox: () => (listboxState.value = ListboxStates.Closed),
|
||||
closeListbox: () => {
|
||||
listboxState.value = ListboxStates.Closed
|
||||
activeOptionIndex.value = null
|
||||
},
|
||||
openListbox: () => (listboxState.value = ListboxStates.Open),
|
||||
goToOption(focus: Focus, id?: string) {
|
||||
const nextActiveOptionIndex = calculateActiveOptionIndex(focus, id)
|
||||
@@ -359,15 +363,11 @@ export const ListboxOptions = defineComponent({
|
||||
props: {
|
||||
as: { type: [Object, String], default: 'ul' },
|
||||
static: { type: Boolean, default: false },
|
||||
unmount: { type: Boolean, default: true },
|
||||
},
|
||||
render() {
|
||||
const api = useListboxContext('ListboxOptions')
|
||||
|
||||
// `static` is a reserved keyword, therefore aliasing it...
|
||||
const { static: isStatic, ...passThroughProps } = this.$props
|
||||
|
||||
if (!isStatic && api.listboxState.value === ListboxStates.Closed) return null
|
||||
|
||||
const slot = { open: api.listboxState.value === ListboxStates.Open }
|
||||
const propsWeControl = {
|
||||
'aria-activedescendant':
|
||||
@@ -381,12 +381,15 @@ export const ListboxOptions = defineComponent({
|
||||
tabIndex: 0,
|
||||
ref: 'el',
|
||||
}
|
||||
const passThroughProps = this.$props
|
||||
|
||||
return render({
|
||||
props: { ...passThroughProps, ...propsWeControl },
|
||||
slot,
|
||||
attrs: this.$attrs,
|
||||
slots: this.$slots,
|
||||
features: Features.RenderStrategy | Features.Static,
|
||||
visible: slot.open,
|
||||
})
|
||||
},
|
||||
setup() {
|
||||
@@ -409,11 +412,11 @@ export const ListboxOptions = defineComponent({
|
||||
// When in type ahead mode, fallthrough
|
||||
case Keys.Enter:
|
||||
event.preventDefault()
|
||||
api.closeListbox()
|
||||
if (api.activeOptionIndex.value !== null) {
|
||||
const { dataRef } = api.options.value[api.activeOptionIndex.value]
|
||||
api.select(dataRef.value)
|
||||
}
|
||||
api.closeListbox()
|
||||
nextTick(() => api.buttonRef.value?.focus())
|
||||
break
|
||||
|
||||
@@ -496,12 +499,20 @@ export const ListboxOption = defineComponent({
|
||||
onUnmounted(() => api.unregisterOption(id))
|
||||
|
||||
onMounted(() => {
|
||||
if (!selected.value) return
|
||||
api.goToOption(Focus.Specific, id)
|
||||
document.getElementById(id)?.focus?.()
|
||||
watch(
|
||||
[api.listboxState, selected],
|
||||
() => {
|
||||
if (api.listboxState.value !== ListboxStates.Open) return
|
||||
if (!selected.value) return
|
||||
api.goToOption(Focus.Specific, id)
|
||||
document.getElementById(id)?.focus?.()
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
})
|
||||
|
||||
watchEffect(() => {
|
||||
if (api.listboxState.value !== ListboxStates.Open) return
|
||||
if (!active.value) return
|
||||
nextTick(() => document.getElementById(id)?.scrollIntoView?.({ block: 'nearest' }))
|
||||
})
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -11,7 +11,7 @@ import {
|
||||
Ref,
|
||||
} from 'vue'
|
||||
import { match } from '../../utils/match'
|
||||
import { render } from '../../utils/render'
|
||||
import { Features, render } from '../../utils/render'
|
||||
import { useId } from '../../hooks/use-id'
|
||||
import { Keys } from '../../keyboard'
|
||||
|
||||
@@ -121,7 +121,10 @@ export const Menu = defineComponent({
|
||||
items,
|
||||
searchQuery,
|
||||
activeItemIndex,
|
||||
closeMenu: () => (menuState.value = MenuStates.Closed),
|
||||
closeMenu: () => {
|
||||
menuState.value = MenuStates.Closed
|
||||
activeItemIndex.value = null
|
||||
},
|
||||
openMenu: () => (menuState.value = MenuStates.Open),
|
||||
goToItem(focus: Focus, id?: string) {
|
||||
const nextActiveItemIndex = calculateActiveItemIndex(focus, id)
|
||||
@@ -280,15 +283,11 @@ export const MenuItems = defineComponent({
|
||||
props: {
|
||||
as: { type: [Object, String], default: 'div' },
|
||||
static: { type: Boolean, default: false },
|
||||
unmount: { type: Boolean, default: true },
|
||||
},
|
||||
render() {
|
||||
const api = useMenuContext('MenuItems')
|
||||
|
||||
// `static` is a reserved keyword, therefore aliasing it...
|
||||
const { static: isStatic, ...passThroughProps } = this.$props
|
||||
|
||||
if (!isStatic && api.menuState.value === MenuStates.Closed) return null
|
||||
|
||||
const slot = { open: api.menuState.value === MenuStates.Open }
|
||||
const propsWeControl = {
|
||||
'aria-activedescendant':
|
||||
@@ -302,12 +301,15 @@ export const MenuItems = defineComponent({
|
||||
tabIndex: 0,
|
||||
ref: 'el',
|
||||
}
|
||||
const passThroughProps = this.$props
|
||||
|
||||
return render({
|
||||
props: { ...passThroughProps, ...propsWeControl },
|
||||
slot,
|
||||
attrs: this.$attrs,
|
||||
slots: this.$slots,
|
||||
features: Features.RenderStrategy | Features.Static,
|
||||
visible: slot.open,
|
||||
})
|
||||
},
|
||||
setup() {
|
||||
@@ -330,11 +332,11 @@ export const MenuItems = defineComponent({
|
||||
// When in type ahead mode, fallthrough
|
||||
case Keys.Enter:
|
||||
event.preventDefault()
|
||||
api.closeMenu()
|
||||
if (api.activeItemIndex.value !== null) {
|
||||
const { id } = api.items.value[api.activeItemIndex.value]
|
||||
document.getElementById(id)?.click()
|
||||
}
|
||||
api.closeMenu()
|
||||
nextTick(() => api.buttonRef.value?.focus())
|
||||
break
|
||||
|
||||
|
||||
@@ -27,8 +27,14 @@ export function getMenuItems(): HTMLElement[] {
|
||||
// ---
|
||||
|
||||
export enum MenuState {
|
||||
Open,
|
||||
Closed,
|
||||
/** The menu is visible to the user. */
|
||||
Visible,
|
||||
|
||||
/** The menu is **not** visible to the user. It's still in the DOM, but it is hidden. */
|
||||
InvisibleHidden,
|
||||
|
||||
/** The menu is **not** visible to the user. It's not in the DOM, it is unmounted. */
|
||||
InvisibleUnmounted,
|
||||
}
|
||||
|
||||
export function assertMenuButton(
|
||||
@@ -47,12 +53,17 @@ export function assertMenuButton(
|
||||
expect(button).toHaveAttribute('aria-haspopup')
|
||||
|
||||
switch (options.state) {
|
||||
case MenuState.Open:
|
||||
case MenuState.Visible:
|
||||
expect(button).toHaveAttribute('aria-controls')
|
||||
expect(button).toHaveAttribute('aria-expanded', 'true')
|
||||
break
|
||||
|
||||
case MenuState.Closed:
|
||||
case MenuState.InvisibleHidden:
|
||||
expect(button).toHaveAttribute('aria-controls')
|
||||
expect(button).not.toHaveAttribute('aria-expanded')
|
||||
break
|
||||
|
||||
case MenuState.InvisibleUnmounted:
|
||||
expect(button).not.toHaveAttribute('aria-controls')
|
||||
expect(button).not.toHaveAttribute('aria-expanded')
|
||||
break
|
||||
@@ -124,27 +135,37 @@ export function assertMenu(
|
||||
) {
|
||||
try {
|
||||
switch (options.state) {
|
||||
case MenuState.Open:
|
||||
case MenuState.InvisibleHidden:
|
||||
if (menu === null) return expect(menu).not.toBe(null)
|
||||
|
||||
// Check that some attributes exists, doesn't really matter what the values are at this point in
|
||||
// time, we just require them.
|
||||
expect(menu).toHaveAttribute('aria-labelledby')
|
||||
assertHidden(menu)
|
||||
|
||||
// Check that we have the correct values for certain attributes
|
||||
expect(menu).toHaveAttribute('aria-labelledby')
|
||||
expect(menu).toHaveAttribute('role', 'menu')
|
||||
|
||||
if (options.textContent) {
|
||||
expect(menu).toHaveTextContent(options.textContent)
|
||||
}
|
||||
if (options.textContent) expect(menu).toHaveTextContent(options.textContent)
|
||||
|
||||
// Ensure menu button has the following attributes
|
||||
for (let attributeName in options.attributes) {
|
||||
expect(menu).toHaveAttribute(attributeName, options.attributes[attributeName])
|
||||
}
|
||||
break
|
||||
|
||||
case MenuState.Closed:
|
||||
case MenuState.Visible:
|
||||
if (menu === null) return expect(menu).not.toBe(null)
|
||||
|
||||
assertVisible(menu)
|
||||
|
||||
expect(menu).toHaveAttribute('aria-labelledby')
|
||||
expect(menu).toHaveAttribute('role', 'menu')
|
||||
|
||||
if (options.textContent) expect(menu).toHaveTextContent(options.textContent)
|
||||
|
||||
for (let attributeName in options.attributes) {
|
||||
expect(menu).toHaveAttribute(attributeName, options.attributes[attributeName])
|
||||
}
|
||||
break
|
||||
|
||||
case MenuState.InvisibleUnmounted:
|
||||
expect(menu).toBe(null)
|
||||
break
|
||||
|
||||
@@ -217,8 +238,14 @@ export function getListboxOptions(): HTMLElement[] {
|
||||
// ---
|
||||
|
||||
export enum ListboxState {
|
||||
Open,
|
||||
Closed,
|
||||
/** The listbox is visible to the user. */
|
||||
Visible,
|
||||
|
||||
/** The listbox is **not** visible to the user. It's still in the DOM, but it is hidden. */
|
||||
InvisibleHidden,
|
||||
|
||||
/** The listbox is **not** visible to the user. It's not in the DOM, it is unmounted. */
|
||||
InvisibleUnmounted,
|
||||
}
|
||||
|
||||
export function assertListbox(
|
||||
@@ -231,27 +258,37 @@ export function assertListbox(
|
||||
) {
|
||||
try {
|
||||
switch (options.state) {
|
||||
case ListboxState.Open:
|
||||
case ListboxState.InvisibleHidden:
|
||||
if (listbox === null) return expect(listbox).not.toBe(null)
|
||||
|
||||
// Check that some attributes exists, doesn't really matter what the values are at this point in
|
||||
// time, we just require them.
|
||||
expect(listbox).toHaveAttribute('aria-labelledby')
|
||||
assertHidden(listbox)
|
||||
|
||||
// Check that we have the correct values for certain attributes
|
||||
expect(listbox).toHaveAttribute('aria-labelledby')
|
||||
expect(listbox).toHaveAttribute('role', 'listbox')
|
||||
|
||||
if (options.textContent) {
|
||||
expect(listbox).toHaveTextContent(options.textContent)
|
||||
}
|
||||
if (options.textContent) expect(listbox).toHaveTextContent(options.textContent)
|
||||
|
||||
// Ensure listbox button has the following attributes
|
||||
for (let attributeName in options.attributes) {
|
||||
expect(listbox).toHaveAttribute(attributeName, options.attributes[attributeName])
|
||||
}
|
||||
break
|
||||
|
||||
case ListboxState.Closed:
|
||||
case ListboxState.Visible:
|
||||
if (listbox === null) return expect(listbox).not.toBe(null)
|
||||
|
||||
assertVisible(listbox)
|
||||
|
||||
expect(listbox).toHaveAttribute('aria-labelledby')
|
||||
expect(listbox).toHaveAttribute('role', 'listbox')
|
||||
|
||||
if (options.textContent) expect(listbox).toHaveTextContent(options.textContent)
|
||||
|
||||
for (let attributeName in options.attributes) {
|
||||
expect(listbox).toHaveAttribute(attributeName, options.attributes[attributeName])
|
||||
}
|
||||
break
|
||||
|
||||
case ListboxState.InvisibleUnmounted:
|
||||
expect(listbox).toBe(null)
|
||||
break
|
||||
|
||||
@@ -280,12 +317,17 @@ export function assertListboxButton(
|
||||
expect(button).toHaveAttribute('aria-haspopup')
|
||||
|
||||
switch (options.state) {
|
||||
case ListboxState.Open:
|
||||
case ListboxState.Visible:
|
||||
expect(button).toHaveAttribute('aria-controls')
|
||||
expect(button).toHaveAttribute('aria-expanded', 'true')
|
||||
break
|
||||
|
||||
case ListboxState.Closed:
|
||||
case ListboxState.InvisibleHidden:
|
||||
expect(button).toHaveAttribute('aria-controls')
|
||||
expect(button).not.toHaveAttribute('aria-expanded')
|
||||
break
|
||||
|
||||
case ListboxState.InvisibleUnmounted:
|
||||
expect(button).not.toHaveAttribute('aria-controls')
|
||||
expect(button).not.toHaveAttribute('aria-expanded')
|
||||
break
|
||||
@@ -567,3 +609,29 @@ export function assertActiveElement(element: HTMLElement | null) {
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
// ---
|
||||
|
||||
export function assertHidden(element: HTMLElement | null) {
|
||||
try {
|
||||
if (element === null) return expect(element).not.toBe(null)
|
||||
|
||||
expect(element).toHaveAttribute('hidden')
|
||||
expect(element).toHaveStyle({ display: 'none' })
|
||||
} catch (err) {
|
||||
Error.captureStackTrace(err, assertHidden)
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
export function assertVisible(element: HTMLElement | null) {
|
||||
try {
|
||||
if (element === null) return expect(element).not.toBe(null)
|
||||
|
||||
expect(element).not.toHaveAttribute('hidden')
|
||||
expect(element).not.toHaveStyle({ display: 'none' })
|
||||
} catch (err) {
|
||||
Error.captureStackTrace(err, assertVisible)
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,73 @@
|
||||
import { h, cloneVNode, Slots } from 'vue'
|
||||
import { match } from './match'
|
||||
|
||||
export enum Features {
|
||||
/** No features at all */
|
||||
None = 0,
|
||||
|
||||
/**
|
||||
* When used, this will allow us to use one of the render strategies.
|
||||
*
|
||||
* **The render strategies are:**
|
||||
* - **Unmount** _(Will unmount the component.)_
|
||||
* - **Hidden** _(Will hide the component using the [hidden] attribute.)_
|
||||
*/
|
||||
RenderStrategy = 1,
|
||||
|
||||
/**
|
||||
* When used, this will allow the user of our component to be in control. This can be used when
|
||||
* you want to transition based on some state.
|
||||
*/
|
||||
Static = 2,
|
||||
}
|
||||
|
||||
enum RenderStrategy {
|
||||
Unmount,
|
||||
Hidden,
|
||||
}
|
||||
|
||||
export function render({
|
||||
visible = true,
|
||||
features = Features.None,
|
||||
...main
|
||||
}: {
|
||||
props: Record<string, any>
|
||||
slot: Record<string, any>
|
||||
attrs: Record<string, any>
|
||||
slots: Slots
|
||||
} & {
|
||||
features?: Features
|
||||
visible?: boolean
|
||||
}) {
|
||||
// Visible always render
|
||||
if (visible) return _render(main)
|
||||
|
||||
if (features & Features.Static) {
|
||||
// When the `static` prop is passed as `true`, then the user is in control, thus we don't care about anything else
|
||||
if (main.props.static) return _render(main)
|
||||
}
|
||||
|
||||
if (features & Features.RenderStrategy) {
|
||||
const strategy = main.props.unmount ?? true ? RenderStrategy.Unmount : RenderStrategy.Hidden
|
||||
|
||||
return match(strategy, {
|
||||
[RenderStrategy.Unmount]() {
|
||||
return null
|
||||
},
|
||||
[RenderStrategy.Hidden]() {
|
||||
return _render({
|
||||
...main,
|
||||
props: { ...main.props, hidden: true, style: { display: 'none' } },
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// No features enabled, just render
|
||||
return _render(main)
|
||||
}
|
||||
|
||||
function _render({
|
||||
props,
|
||||
attrs,
|
||||
slots,
|
||||
@@ -11,7 +78,7 @@ export function render({
|
||||
attrs: Record<string, any>
|
||||
slots: Slots
|
||||
}) {
|
||||
const { as, ...passThroughProps } = props
|
||||
const { as, ...passThroughProps } = omit(props, ['unmount', 'static'])
|
||||
|
||||
const children = slots.default?.(slot)
|
||||
|
||||
@@ -30,3 +97,11 @@ export function render({
|
||||
|
||||
return h(as, passThroughProps, children)
|
||||
}
|
||||
|
||||
function omit<T extends Record<any, any>>(object: T, keysToOmit: string[] = []) {
|
||||
let clone = Object.assign({}, object)
|
||||
for (let key of keysToOmit) {
|
||||
if (key in clone) delete clone[key]
|
||||
}
|
||||
return clone
|
||||
}
|
||||
|
||||
@@ -363,6 +363,11 @@
|
||||
resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.11.5.tgz#c7ff6303df71080ec7a4f5b8c003c58f1cf51037"
|
||||
integrity sha512-X9rD8qqm695vgmeaQ4fvz/o3+Wk4ZzQvSHkDBgpYKxpD4qTAUm88ZKtHkVqIOsYFFbIQ6wQYhC6q7pjqVK0E0Q==
|
||||
|
||||
"@babel/parser@^7.12.0":
|
||||
version "7.12.3"
|
||||
resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.12.3.tgz#a305415ebe7a6c7023b40b5122a0662d928334cd"
|
||||
integrity sha512-kFsOS0IbsuhO5ojF8Hc8z/8vEIOkylVBrjiZUbLTE3XFe0Qi+uu6HjzQixkFaqr0ZPAMZcBVxEwmsnsLPZ2Xsw==
|
||||
|
||||
"@babel/plugin-proposal-async-generator-functions@^7.10.4":
|
||||
version "7.10.5"
|
||||
resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.10.5.tgz#3491cabf2f7c179ab820606cec27fed15e0e8558"
|
||||
@@ -1065,6 +1070,15 @@
|
||||
lodash "^4.17.13"
|
||||
to-fast-properties "^2.0.0"
|
||||
|
||||
"@babel/types@^7.12.0":
|
||||
version "7.12.1"
|
||||
resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.12.1.tgz#e109d9ab99a8de735be287ee3d6a9947a190c4ae"
|
||||
integrity sha512-BzSY3NJBKM4kyatSOWh3D/JJ2O3CVzBybHWxtgxnggaxEuaSTTDqeiSb/xk9lrkw2Tbqyivw5ZU4rT+EfznQsA==
|
||||
dependencies:
|
||||
"@babel/helper-validator-identifier" "^7.10.4"
|
||||
lodash "^4.17.19"
|
||||
to-fast-properties "^2.0.0"
|
||||
|
||||
"@bcoe/v8-coverage@^0.2.3":
|
||||
version "0.2.3"
|
||||
resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39"
|
||||
@@ -1491,10 +1505,10 @@
|
||||
hex-rgb "^4.1.0"
|
||||
postcss-selector-parser "^6.0.2"
|
||||
|
||||
"@testing-library/dom@^7.24.2", "@testing-library/dom@^7.5.7":
|
||||
version "7.24.2"
|
||||
resolved "https://registry.yarnpkg.com/@testing-library/dom/-/dom-7.24.2.tgz#6d2b7dd21efbd5358b98c2777fc47c252f3ae55e"
|
||||
integrity sha512-ERxcZSoHx0EcN4HfshySEWmEf5Kkmgi+J7O79yCJ3xggzVlBJ2w/QjJUC+EBkJJ2OeSw48i3IoePN4w8JlVUIA==
|
||||
"@testing-library/dom@^7.24.3":
|
||||
version "7.24.3"
|
||||
resolved "https://registry.yarnpkg.com/@testing-library/dom/-/dom-7.24.3.tgz#dae3071463cf28dc7755b43d9cf2202e34cbb85d"
|
||||
integrity sha512-6eW9fUhEbR423FZvoHRwbWm9RUUByLWGayYFNVvqTnQLYvsNpBS4uEuKH9aqr3trhxFwGVneJUonehL3B1sHJw==
|
||||
dependencies:
|
||||
"@babel/code-frame" "^7.10.4"
|
||||
"@babel/runtime" "^7.10.3"
|
||||
@@ -1504,10 +1518,24 @@
|
||||
dom-accessibility-api "^0.5.1"
|
||||
pretty-format "^26.4.2"
|
||||
|
||||
"@testing-library/dom@^7.24.3":
|
||||
version "7.24.3"
|
||||
resolved "https://registry.yarnpkg.com/@testing-library/dom/-/dom-7.24.3.tgz#dae3071463cf28dc7755b43d9cf2202e34cbb85d"
|
||||
integrity sha512-6eW9fUhEbR423FZvoHRwbWm9RUUByLWGayYFNVvqTnQLYvsNpBS4uEuKH9aqr3trhxFwGVneJUonehL3B1sHJw==
|
||||
"@testing-library/dom@^7.26.0":
|
||||
version "7.26.0"
|
||||
resolved "https://registry.yarnpkg.com/@testing-library/dom/-/dom-7.26.0.tgz#da4d052dc426a4ccc916303369c6e7552126f680"
|
||||
integrity sha512-fyKFrBbS1IigaE3FV21LyeC7kSGF84lqTlSYdKmGaHuK2eYQ/bXVPM5vAa2wx/AU1iPD6oQHsxy2QQ17q9AMCg==
|
||||
dependencies:
|
||||
"@babel/code-frame" "^7.10.4"
|
||||
"@babel/runtime" "^7.10.3"
|
||||
"@types/aria-query" "^4.2.0"
|
||||
aria-query "^4.2.2"
|
||||
chalk "^4.1.0"
|
||||
dom-accessibility-api "^0.5.1"
|
||||
lz-string "^1.4.4"
|
||||
pretty-format "^26.4.2"
|
||||
|
||||
"@testing-library/dom@^7.5.7":
|
||||
version "7.24.2"
|
||||
resolved "https://registry.yarnpkg.com/@testing-library/dom/-/dom-7.24.2.tgz#6d2b7dd21efbd5358b98c2777fc47c252f3ae55e"
|
||||
integrity sha512-ERxcZSoHx0EcN4HfshySEWmEf5Kkmgi+J7O79yCJ3xggzVlBJ2w/QjJUC+EBkJJ2OeSw48i3IoePN4w8JlVUIA==
|
||||
dependencies:
|
||||
"@babel/code-frame" "^7.10.4"
|
||||
"@babel/runtime" "^7.10.3"
|
||||
@@ -1531,13 +1559,13 @@
|
||||
lodash "^4.17.15"
|
||||
redent "^3.0.0"
|
||||
|
||||
"@testing-library/react@^11.0.4":
|
||||
version "11.0.4"
|
||||
resolved "https://registry.yarnpkg.com/@testing-library/react/-/react-11.0.4.tgz#c84082bfe1593d8fcd475d46baee024452f31dee"
|
||||
integrity sha512-U0fZO2zxm7M0CB5h1+lh31lbAwMSmDMEMGpMT3BUPJwIjDEKYWOV4dx7lb3x2Ue0Pyt77gmz/VropuJnSz/Iew==
|
||||
"@testing-library/react@^11.1.0":
|
||||
version "11.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@testing-library/react/-/react-11.1.0.tgz#dfb4b3177d05a8ccf156b5fd14a5550e91d7ebe4"
|
||||
integrity sha512-Nfz58jGzW0tgg3irmTB7sa02JLkLnCk+QN3XG6WiaGQYb0Qc4Ok00aujgjdxlIQWZHbb4Zj5ZOIeE9yKFSs4sA==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.11.2"
|
||||
"@testing-library/dom" "^7.24.2"
|
||||
"@testing-library/dom" "^7.26.0"
|
||||
|
||||
"@testing-library/vue@^5.1.0":
|
||||
version "5.1.0"
|
||||
@@ -1779,10 +1807,10 @@
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-14.11.1.tgz#56af902ad157e763f9ba63d671c39cda3193c835"
|
||||
integrity sha512-oTQgnd0hblfLsJ6BvJzzSL+Inogp3lq9fGgqRkMB/ziKMgEUaFl801OncOzUmalfzt14N0oPHMK47ipl+wbTIw==
|
||||
|
||||
"@types/node@^14.11.8":
|
||||
version "14.11.8"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-14.11.8.tgz#fe2012f2355e4ce08bca44aeb3abbb21cf88d33f"
|
||||
integrity sha512-KPcKqKm5UKDkaYPTuXSx8wEP7vE9GnuaXIZKijwRYcePpZFDVuy2a57LarFKiORbHOuTOOwYzxVxcUzsh2P2Pw==
|
||||
"@types/node@^14.11.10":
|
||||
version "14.11.10"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-14.11.10.tgz#8c102aba13bf5253f35146affbf8b26275069bef"
|
||||
integrity sha512-yV1nWZPlMFpoXyoknm4S56y2nlTAuFYaJuQtYRAOU7xA/FJ9RY0Xm7QOkaYMMmr8ESdHIuUb6oQgR/0+2NqlyA==
|
||||
|
||||
"@types/normalize-package-data@^2.4.0":
|
||||
version "2.4.0"
|
||||
@@ -1834,10 +1862,10 @@
|
||||
"@types/prop-types" "*"
|
||||
csstype "^3.0.2"
|
||||
|
||||
"@types/react@^16.9.52":
|
||||
version "16.9.52"
|
||||
resolved "https://registry.yarnpkg.com/@types/react/-/react-16.9.52.tgz#c46c72d1a1d8d9d666f4dd2066c0e22600ccfde1"
|
||||
integrity sha512-EHRjmnxiNivwhGdMh9sz1Yw9AUxTSZFxKqdBWAAzyZx3sufWwx6ogqHYh/WB1m/I4ZpjkoZLExF5QTy2ekVi/Q==
|
||||
"@types/react@^16.9.53":
|
||||
version "16.9.53"
|
||||
resolved "https://registry.yarnpkg.com/@types/react/-/react-16.9.53.tgz#40cd4f8b8d6b9528aedd1fff8fcffe7a112a3d23"
|
||||
integrity sha512-4nW60Sd4L7+WMXH1D6jCdVftuW7j4Za6zdp6tJ33Rqv0nk1ZAmQKML9ZLD4H0dehA3FZxXR/GM8gXplf82oNGw==
|
||||
dependencies:
|
||||
"@types/prop-types" "*"
|
||||
csstype "^3.0.2"
|
||||
@@ -1947,6 +1975,17 @@
|
||||
estree-walker "^2.0.1"
|
||||
source-map "^0.6.1"
|
||||
|
||||
"@vue/compiler-core@3.0.1":
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@vue/compiler-core/-/compiler-core-3.0.1.tgz#3ce57531078c6220be7ea458e41e4bab3522015b"
|
||||
integrity sha512-BbQQj9YVNaNWEPnP4PiFKgW8QSGB3dcPSKCtekx1586m4VA1z8hHNLQnzeygtV8BM4oU6yriiWmOIYghbJHwFw==
|
||||
dependencies:
|
||||
"@babel/parser" "^7.12.0"
|
||||
"@babel/types" "^7.12.0"
|
||||
"@vue/shared" "3.0.1"
|
||||
estree-walker "^2.0.1"
|
||||
source-map "^0.6.1"
|
||||
|
||||
"@vue/compiler-dom@3.0.0", "@vue/compiler-dom@^3.0.0-rc.5":
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@vue/compiler-dom/-/compiler-dom-3.0.0.tgz#4cbb48fcf1f852daef2babcf9953b681ac463526"
|
||||
@@ -1955,7 +1994,37 @@
|
||||
"@vue/compiler-core" "3.0.0"
|
||||
"@vue/shared" "3.0.0"
|
||||
|
||||
"@vue/compiler-sfc@3.0.0", "@vue/compiler-sfc@^3.0.0-rc.5":
|
||||
"@vue/compiler-dom@3.0.1":
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@vue/compiler-dom/-/compiler-dom-3.0.1.tgz#00b12f2e4aa55e624e2a5257e4bed93cf7555f0b"
|
||||
integrity sha512-8cjgswVU2YmV35H9ARZmSlDr1P9VZxUihRwefkrk6Vrsb7kui5C3d/WQ2/su34FSDpyMU1aacUOiL2CV/vdX6w==
|
||||
dependencies:
|
||||
"@vue/compiler-core" "3.0.1"
|
||||
"@vue/shared" "3.0.1"
|
||||
|
||||
"@vue/compiler-sfc@3.0.1":
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@vue/compiler-sfc/-/compiler-sfc-3.0.1.tgz#f340f8f75b5c1c4509e0f3a12c79d1544899b663"
|
||||
integrity sha512-VO5gJ7SyHw0hf1rkKXRlxjXI9+Q4ngcuUWYnyjOSDch7Wtt2IdOEiC82KFWIkfWMpHqA5HPzL2nDmys3y9d19w==
|
||||
dependencies:
|
||||
"@babel/parser" "^7.12.0"
|
||||
"@babel/types" "^7.12.0"
|
||||
"@vue/compiler-core" "3.0.1"
|
||||
"@vue/compiler-dom" "3.0.1"
|
||||
"@vue/compiler-ssr" "3.0.1"
|
||||
"@vue/shared" "3.0.1"
|
||||
consolidate "^0.16.0"
|
||||
estree-walker "^2.0.1"
|
||||
hash-sum "^2.0.0"
|
||||
lru-cache "^5.1.1"
|
||||
magic-string "^0.25.7"
|
||||
merge-source-map "^1.1.0"
|
||||
postcss "^7.0.32"
|
||||
postcss-modules "^3.2.2"
|
||||
postcss-selector-parser "^6.0.4"
|
||||
source-map "^0.6.1"
|
||||
|
||||
"@vue/compiler-sfc@^3.0.0-rc.5":
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@vue/compiler-sfc/-/compiler-sfc-3.0.0.tgz#efa38037984bd64aae315828aa5c1248c6eadca9"
|
||||
integrity sha512-1Bn4L5jNRm6tlb79YwqYUGGe+Yc9PRoRSJi67NJX6icdhf84+tRMtESbx1zCLL9QixQXu2+7aLkXHxvh4RpqAA==
|
||||
@@ -1985,6 +2054,14 @@
|
||||
"@vue/compiler-dom" "3.0.0"
|
||||
"@vue/shared" "3.0.0"
|
||||
|
||||
"@vue/compiler-ssr@3.0.1":
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@vue/compiler-ssr/-/compiler-ssr-3.0.1.tgz#0455b011d72d4ed02faa93610f14981c3d44a079"
|
||||
integrity sha512-U0Vb7BOniw9rY0/YvXNw5EuIuO0dCoZd3XhnDjAKL9A5pSBxTlx6fPJeQ53gV0XH40M5z8q4yXukFqSVTXC6hQ==
|
||||
dependencies:
|
||||
"@vue/compiler-dom" "3.0.1"
|
||||
"@vue/shared" "3.0.1"
|
||||
|
||||
"@vue/reactivity@3.0.0":
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@vue/reactivity/-/reactivity-3.0.0.tgz#fd15632a608650ce2a969c721787e27e2c80aa6b"
|
||||
@@ -2014,6 +2091,11 @@
|
||||
resolved "https://registry.yarnpkg.com/@vue/shared/-/shared-3.0.0.tgz#ec089236629ecc0f10346b92f101ff4339169f1a"
|
||||
integrity sha512-4XWL/avABGxU2E2ZF1eZq3Tj7fvksCMssDZUHOykBIMmh5d+KcAnQMC5XHMhtnA0NAvktYsA2YpdsVwVmhWzvA==
|
||||
|
||||
"@vue/shared@3.0.1":
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@vue/shared/-/shared-3.0.1.tgz#48196c056726aa7466d0182698524c84f203006b"
|
||||
integrity sha512-/X6AUbTFCyD2BcJnBoacUct8qcv1A5uk1+N+3tbzDVuhGPRmoYrTSnNUuF53C/GIsTkChrEiXaJh2kyo/0tRvw==
|
||||
|
||||
"@vue/test-utils@^1.0.3", "@vue/test-utils@^1.1.0":
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@vue/test-utils/-/test-utils-1.1.0.tgz#76305e73a786c921ede1352849614e26c7113f94"
|
||||
@@ -2023,10 +2105,10 @@
|
||||
lodash "^4.17.15"
|
||||
pretty "^2.0.0"
|
||||
|
||||
"@vue/test-utils@^2.0.0-beta.6":
|
||||
version "2.0.0-beta.6"
|
||||
resolved "https://registry.yarnpkg.com/@vue/test-utils/-/test-utils-2.0.0-beta.6.tgz#2f7a653b0025cd4236968269c5972e807fa1fb2c"
|
||||
integrity sha512-nBj5HHoTD+2xg0OQ93p/Hil5SkFUcNJ5BA2RUnHlOH6a4PVskgMK8dOLyVcZ1ZJif7knjt7yQVJ6K6YwIzeR1A==
|
||||
"@vue/test-utils@^2.0.0-beta.7":
|
||||
version "2.0.0-beta.7"
|
||||
resolved "https://registry.yarnpkg.com/@vue/test-utils/-/test-utils-2.0.0-beta.7.tgz#27751991e0b013ee4af487e51e16a58d477e5857"
|
||||
integrity sha512-cAe7VqoxxkxTr/2N93UpW/LQbcUVKC+QRA3ZBq5ZWImtAf/8jtcdC2mQ9g4AKmSvyaKQtqxrRn4i/y5z7yrrKA==
|
||||
|
||||
"@webassemblyjs/ast@1.9.0":
|
||||
version "1.9.0"
|
||||
@@ -6748,10 +6830,10 @@ lines-and-columns@^1.1.6:
|
||||
resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.1.6.tgz#1c00c743b433cd0a4e80758f7b64a57440d9ff00"
|
||||
integrity sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA=
|
||||
|
||||
lint-staged@^10.4.0:
|
||||
version "10.4.0"
|
||||
resolved "https://registry.yarnpkg.com/lint-staged/-/lint-staged-10.4.0.tgz#d18628f737328e0bbbf87d183f4020930e9a984e"
|
||||
integrity sha512-uaiX4U5yERUSiIEQc329vhCTDDwUcSvKdRLsNomkYLRzijk3v8V9GWm2Nz0RMVB87VcuzLvtgy6OsjoH++QHIg==
|
||||
lint-staged@^10.4.2:
|
||||
version "10.4.2"
|
||||
resolved "https://registry.yarnpkg.com/lint-staged/-/lint-staged-10.4.2.tgz#9fee4635c4b5ddb845746f237c6d43494ccd21c1"
|
||||
integrity sha512-OLCA9K1hS+Sl179SO6kX0JtnsaKj/MZalEhUj5yAgXsb63qPI/Gfn6Ua1KuZdbfkZNEu3/n5C/obYCu70IMt9g==
|
||||
dependencies:
|
||||
chalk "^4.1.0"
|
||||
cli-truncate "^2.1.0"
|
||||
@@ -6959,6 +7041,11 @@ lru-cache@^5.1.1:
|
||||
dependencies:
|
||||
yallist "^3.0.2"
|
||||
|
||||
lz-string@^1.4.4:
|
||||
version "1.4.4"
|
||||
resolved "https://registry.yarnpkg.com/lz-string/-/lz-string-1.4.4.tgz#c0d8eaf36059f705796e1e344811cf4c498d3a26"
|
||||
integrity sha1-wNjq82BZ9wV5bh40SBHPTEmNOiY=
|
||||
|
||||
magic-string@^0.25.2, magic-string@^0.25.5, magic-string@^0.25.7:
|
||||
version "0.25.7"
|
||||
resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.25.7.tgz#3f497d6fd34c669c6798dcb821f2ef31f5445051"
|
||||
@@ -8147,6 +8234,16 @@ postcss-selector-parser@^6.0.0, postcss-selector-parser@^6.0.2:
|
||||
indexes-of "^1.0.1"
|
||||
uniq "^1.0.1"
|
||||
|
||||
postcss-selector-parser@^6.0.4:
|
||||
version "6.0.4"
|
||||
resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.4.tgz#56075a1380a04604c38b063ea7767a129af5c2b3"
|
||||
integrity sha512-gjMeXBempyInaBqpp8gODmwZ52WaYsVOsfr4L4lDQ7n3ncD6mEyySiDtgzCT+NYC0mmeOLvtsF8iaEf0YT6dBw==
|
||||
dependencies:
|
||||
cssesc "^3.0.0"
|
||||
indexes-of "^1.0.1"
|
||||
uniq "^1.0.1"
|
||||
util-deprecate "^1.0.2"
|
||||
|
||||
postcss-value-parser@^3.2.3, postcss-value-parser@^3.3.0:
|
||||
version "3.3.1"
|
||||
resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz#9ff822547e2893213cf1c30efa51ac5fd1ba8281"
|
||||
@@ -8410,10 +8507,10 @@ randomfill@^1.0.3:
|
||||
randombytes "^2.0.5"
|
||||
safe-buffer "^5.1.0"
|
||||
|
||||
react-dom@^16.13.1:
|
||||
version "16.13.1"
|
||||
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.13.1.tgz#c1bd37331a0486c078ee54c4740720993b2e0e7f"
|
||||
integrity sha512-81PIMmVLnCNLO/fFOQxdQkvEq/+Hfpv24XNJfpyZhTRfO0QcmQIF/PgCa1zCOj2w1hrn12MFLyaJ/G0+Mxtfag==
|
||||
react-dom@^16.14.0:
|
||||
version "16.14.0"
|
||||
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.14.0.tgz#7ad838ec29a777fb3c75c3a190f661cf92ab8b89"
|
||||
integrity sha512-1gCeQXDLoIqMgqD3IO2Ah9bnf0w9kzhwN5q4FGnHZ67hBm9yePzB5JJAIQCc8x3pFnNlwFq4RidZggNAAkzWWw==
|
||||
dependencies:
|
||||
loose-envify "^1.1.0"
|
||||
object-assign "^4.1.1"
|
||||
@@ -8430,10 +8527,10 @@ react-refresh@0.8.3:
|
||||
resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.8.3.tgz#721d4657672d400c5e3c75d063c4a85fb2d5d68f"
|
||||
integrity sha512-X8jZHc7nCMjaCqoU+V2I0cOhNW+QMBwSUkeXnTi8IPe6zaRWfn60ZzvFDZqWPfmSJfjub7dDW1SP0jaHWLu/hg==
|
||||
|
||||
react@^16.13.1:
|
||||
version "16.13.1"
|
||||
resolved "https://registry.yarnpkg.com/react/-/react-16.13.1.tgz#2e818822f1a9743122c063d6410d85c1e3afe48e"
|
||||
integrity sha512-YMZQQq32xHLX0bz5Mnibv1/LHb3Sqzngu7xstSM+vrkE5Kzr9xE0yMByK5kMoTK30YVJE61WfbxIFFvfeDKT1w==
|
||||
react@^16.14.0:
|
||||
version "16.14.0"
|
||||
resolved "https://registry.yarnpkg.com/react/-/react-16.14.0.tgz#94d776ddd0aaa37da3eda8fc5b6b18a4c9a3114d"
|
||||
integrity sha512-0X2CImDkJGApiAlcf0ODKIneSwBPhqJawOa5wCtKbu7ZECrmS26NvtSILynQ66cgkT/RJ4LidJOc3bUESwmU8g==
|
||||
dependencies:
|
||||
loose-envify "^1.1.0"
|
||||
object-assign "^4.1.1"
|
||||
@@ -9700,10 +9797,10 @@ table@^5.2.3:
|
||||
slice-ansi "^2.1.0"
|
||||
string-width "^3.0.0"
|
||||
|
||||
tailwindcss@^1.9.1:
|
||||
version "1.9.1"
|
||||
resolved "https://registry.yarnpkg.com/tailwindcss/-/tailwindcss-1.9.1.tgz#5cd83b7962c0e22d7608bc502daf4185962995fc"
|
||||
integrity sha512-3faxlyPlcWN8AoNEIVQFNsDcrdXS/D9nOGtdknrXvZp4D4E3AGPO2KRPiGG69B2ZUO0V6RvYiW91L2/n9QnBxg==
|
||||
tailwindcss@^1.9.4:
|
||||
version "1.9.4"
|
||||
resolved "https://registry.yarnpkg.com/tailwindcss/-/tailwindcss-1.9.4.tgz#5ae8ff84bc8234df22ba5f2c7feafb64bb14da55"
|
||||
integrity sha512-CVeP4J1pDluBM/AF11JPku9Cx+VwQ6MbOcnlobnWVVZnq+xku8sa+XXmYzy/GvE08qD8w+OmpSdN21ZFPoVDRg==
|
||||
dependencies:
|
||||
"@fullhuman/postcss-purgecss" "^2.1.2"
|
||||
autoprefixer "^9.4.5"
|
||||
@@ -10242,7 +10339,7 @@ use@^3.1.0:
|
||||
resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f"
|
||||
integrity sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==
|
||||
|
||||
util-deprecate@^1.0.1, util-deprecate@~1.0.1:
|
||||
util-deprecate@^1.0.1, util-deprecate@^1.0.2, util-deprecate@~1.0.1:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
|
||||
integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=
|
||||
|
||||
Reference in New Issue
Block a user