Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Transition events #57

Merged
merged 4 commits into from
Oct 8, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 15 additions & 11 deletions packages/@headlessui-react/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -312,17 +312,21 @@ function MyComponent({ isShowing }) {

##### Props

| Prop | Type | Description |
| ----------- | ------------------------------------- | ------------------------------------------------------------------------------------- |
| `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. |
| `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. |
| `leave` | String _(Default: '')_ | Classes to add to the transitioning element during the entire leave phase. |
| `leaveFrom` | String _(Default: '')_ | Classes to add to the transitioning element before the leave phase starts. |
| `leaveTo` | String _(Default: '')_ | Classes to add to the transitioning element immediately after the leave phase starts. |
| Prop | Type | Description |
| ------------- | ------------------------------------- | ------------------------------------------------------------------------------------- |
| `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. |
| `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. |
| `leave` | String _(Default: '')_ | Classes to add to the transitioning element during the entire leave phase. |
| `leaveFrom` | String _(Default: '')_ | Classes to add to the transitioning element before the leave phase starts. |
| `leaveTo` | String _(Default: '')_ | Classes to add to the transitioning element immediately after the leave phase starts. |
| `beforeEnter` | Function | Callback which is called before we start the enter transition. |
| `afterEnter` | Function | Callback which is called after we finished the enter transition. |
| `beforeLeave` | Function | Callback which is called before we start the leave transition. |
| `afterLeave` | Function | Callback which is called after we finished the leave transition. |

##### Render prop arguments

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
import React, { useRef, useState } from 'react'
import { Transition } from '@headlessui/react'

export default function Home() {
const [isOpen, setIsOpen] = useState(false)
function toggle() {
setIsOpen(v => !v)
}

const [email, setEmail] = useState('')
const [events, setEvents] = useState([])
const inputRef = useRef(null)

function addEvent(name) {
setEvents(existing => [...existing, `${new Date().toJSON()} - ${name}`])
}

return (
<div>
<div className="flex p-12 space-x-4">
<div className="inline-block p-12">
<span className="flex w-full mt-3 rounded-md shadow-sm sm:mt-0 sm:w-auto">
<button
onClick={toggle}
type="button"
className="inline-flex justify-center w-full px-4 py-2 text-base font-medium leading-6 text-gray-700 transition duration-150 ease-in-out bg-white border border-gray-300 rounded-md shadow-sm hover:text-gray-500 focus:outline-none focus:border-blue-300 focus:shadow-outline-blue sm:text-sm sm:leading-5"
>
Show modal
</button>
</span>
</div>

<ul className="p-4 text-gray-900 bg-gray-200">
<h3 className="font-bold">Events:</h3>
{events.map((event, i) => (
<li key={i} className="font-mono text-sm">
{event}
</li>
))}
</ul>
</div>

<Transition
show={isOpen}
className="fixed inset-0 z-10 overflow-y-auto"
beforeEnter={() => {
addEvent('Before enter')
}}
afterEnter={() => {
inputRef.current.focus()
addEvent('After enter')
}}
beforeLeave={() => {
addEvent('Before leave (before confirm)')
window.confirm('Are you sure?')
addEvent('Before leave (after confirm)')
}}
afterLeave={() => {
addEvent('After leave (before alert)')
window.alert('Consider it done!')
addEvent('After leave (after alert)')
setEmail('')
}}
>
<div className="flex items-end justify-center min-h-screen px-4 pt-4 pb-20 text-center sm:block sm:p-0">
<Transition.Child
enter="ease-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-in duration-200"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="fixed inset-0 transition-opacity">
<div className="absolute inset-0 bg-gray-500 opacity-75"></div>
</div>
</Transition.Child>
{/* This element is to trick the browser into centering the modal contents. */}
<span className="hidden sm:inline-block sm:align-middle sm:h-screen"></span>&#8203;
<Transition.Child
className="inline-block overflow-hidden text-left align-bottom transition-all transform bg-white rounded-lg shadow-xl sm:my-8 sm:align-middle sm:max-w-lg sm:w-full"
role="dialog"
aria-modal="true"
aria-labelledby="modal-headline"
enter="ease-out duration-300"
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
enterTo="opacity-100 translate-y-0 sm:scale-100"
leave="ease-in duration-200"
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
>
<div className="px-4 pt-5 pb-4 bg-white sm:p-6 sm:pb-4">
<div className="sm:flex sm:items-start">
<div className="flex items-center justify-center flex-shrink-0 w-12 h-12 mx-auto bg-red-100 rounded-full sm:mx-0 sm:h-10 sm:w-10">
{/* Heroicon name: exclamation */}
<svg
className="w-6 h-6 text-red-600"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"
/>
</svg>
</div>
<div className="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
<h3 className="text-lg font-medium leading-6 text-gray-900" id="modal-headline">
Deactivate account
</h3>
<div className="mt-2">
<p className="text-sm leading-5 text-gray-500">
Are you sure you want to deactivate your account? All of your data will be
permanently removed. This action cannot be undone.
</p>
</div>
<div className="mt-2">
<div>
<label
htmlFor="email"
className="block text-sm font-medium leading-5 text-gray-700"
>
Email address
</label>
<div className="relative mt-1 rounded-md shadow-sm">
<input
ref={inputRef}
value={email}
onChange={event => setEmail(event.target.value)}
id="email"
className="block w-full px-3 form-input sm:text-sm sm:leading-5"
placeholder="name@example.com"
/>
</div>
</div>
</div>
</div>
</div>
</div>
<div className="px-4 py-3 bg-gray-50 sm:px-6 sm:flex sm:flex-row-reverse">
<span className="flex w-full rounded-md shadow-sm sm:ml-3 sm:w-auto">
<button
type="button"
className="inline-flex justify-center w-full px-4 py-2 text-base font-medium leading-6 text-white transition duration-150 ease-in-out bg-red-600 border border-transparent rounded-md shadow-sm hover:bg-red-500 focus:outline-none focus:border-red-700 focus:shadow-outline-red sm:text-sm sm:leading-5"
>
Deactivate
</button>
</span>
<span className="flex w-full mt-3 rounded-md shadow-sm sm:mt-0 sm:w-auto">
<button
onClick={toggle}
type="button"
className="inline-flex justify-center w-full px-4 py-2 text-base font-medium leading-6 text-gray-700 transition duration-150 ease-in-out bg-white border border-gray-300 rounded-md shadow-sm hover:text-gray-500 focus:outline-none focus:border-blue-300 focus:shadow-outline-blue sm:text-sm sm:leading-5"
>
Cancel
</button>
</span>
</div>
</Transition.Child>
</div>
</Transition>
</div>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -412,7 +412,7 @@ describe('Transitions', () => {

return (
<>
<style>{`.enter { transition-duration: ${enterDuration}ms; } .from { opacity: 0%; } .from { opacity: 100%; }`}</style>
<style>{`.enter { transition-duration: ${enterDuration}ms; } .from { opacity: 0%; } .to { opacity: 100%; }`}</style>

<Transition show={show} enter="enter" enterFrom="from" enterTo="to">
<span>Hello!</span>
Expand Down Expand Up @@ -462,7 +462,7 @@ describe('Transitions', () => {
return (
<>
<style>{`.enter { transition-duration: ${enterDuration /
1000}s; } .from { opacity: 0%; } .from { opacity: 100%; }`}</style>
1000}s; } .from { opacity: 0%; } .to { opacity: 100%; }`}</style>

<Transition show={show} enter="enter" enterFrom="from" enterTo="to">
<span>Hello!</span>
Expand Down Expand Up @@ -511,7 +511,7 @@ describe('Transitions', () => {

return (
<>
<style>{`.enter { transition-duration: ${enterDuration}ms; } .from { opacity: 0%; } .from { opacity: 100%; }`}</style>
<style>{`.enter { transition-duration: ${enterDuration}ms; } .from { opacity: 0%; } .to { opacity: 100%; }`}</style>

<Transition show={show} enter="enter" enterFrom="from" enterTo="to">
<span>Hello!</span>
Expand Down Expand Up @@ -871,3 +871,118 @@ describe('Transitions', () => {
)
})
})

describe('Events', () => {
it(
'should fire events for all the stages',
suppressConsoleLogs(async () => {
const eventHandler = jest.fn()
const enterDuration = 50
const leaveDuration = 75

function Example() {
const [show, setShow] = React.useState(false)
const start = React.useRef(Date.now())

React.useLayoutEffect(() => {
start.current = Date.now()
}, [])

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}
// Events
beforeEnter={() => eventHandler('beforeEnter', Date.now() - start.current)}
afterEnter={() => eventHandler('afterEnter', Date.now() - start.current)}
beforeLeave={() => eventHandler('beforeLeave', Date.now() - start.current)}
afterLeave={() => eventHandler('afterLeave', Date.now() - start.current)}
// Class names
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)
},
])

expect(timeline).toMatchInlineSnapshot(`
"Render 1:
+ <div
+ class=\\"enter enter-from\\"
+ >
+ <span>
+ Hello!
+ </span>
+ </div>

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)
- <div
- class=\\"leave leave-to\\"
- >
- <span>
- Hello!
- </span>
- </div>"
`)

expect(eventHandler).toHaveBeenCalledTimes(4)
expect(eventHandler.mock.calls.map(([name]) => name)).toEqual([
// Order is important here
'beforeEnter',
'afterEnter',
'beforeLeave',
'afterLeave',
])

const enterHookDiff = eventHandler.mock.calls[1][1] - eventHandler.mock.calls[0][1]
expect(enterHookDiff).toBeGreaterThanOrEqual(enterDuration)
expect(enterHookDiff).toBeLessThanOrEqual(enterDuration * 2)

const leaveHookDiff = eventHandler.mock.calls[3][1] - eventHandler.mock.calls[2][1]
expect(leaveHookDiff).toBeGreaterThanOrEqual(leaveDuration)
expect(leaveHookDiff).toBeLessThanOrEqual(leaveDuration * 2)
})
)
})
Loading