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

Fix Transition events order #1585

Closed
wants to merge 29 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
d463277
add `defer` utility
RobinMalfait Jun 13, 2022
41ebef7
remove events from `useTransition` hook
RobinMalfait Jun 13, 2022
36b0d23
fix `beforeEnter`, `afterEnter`, `beforeLeave` and `afterLeave` event
RobinMalfait Jun 13, 2022
5cf3924
refactor: use eventChain instead of Tree
RobinMalfait Jun 13, 2022
12c4224
ensure cancelling works properly
RobinMalfait Jun 13, 2022
6d140c6
update changelog
RobinMalfait Jun 13, 2022
1e2f1e5
WIP
RobinMalfait Jun 14, 2022
cbbaed7
[WIP]: temporarily disable strict mode
RobinMalfait Jun 14, 2022
bb95aca
fix a timing issue
RobinMalfait Jun 15, 2022
6dba416
WIP
thecrypticace Jun 16, 2022
6594bd8
WIP
thecrypticace Jun 19, 2022
f4e0076
WIP
thecrypticace Jun 19, 2022
47f8029
WIP
thecrypticace Jun 20, 2022
75d138a
fix incorrect import
RobinMalfait Jun 20, 2022
6dcc2ea
remove clever use of `void`
RobinMalfait Jun 20, 2022
ce30396
Remove old deferred node stuff
thecrypticace Jun 29, 2022
7d08c46
Change how state machines are debugged
thecrypticace Jun 29, 2022
d8bc8f0
Remove onChildStop
thecrypticace Jun 29, 2022
3977cf2
Fix bug where onStart doesn’t fire for root after unmounting and remo…
thecrypticace Jun 29, 2022
205b32c
Cleanup
thecrypticace Jun 29, 2022
9980892
Cleanup debugging code a bit
thecrypticace Jun 29, 2022
9a5a358
Move stuff around a bit
thecrypticace Jun 30, 2022
b4da716
WIP
thecrypticace Jun 30, 2022
6ac84ad
Remove deferred node concept
thecrypticace Jun 30, 2022
a6d7ada
WIP extract state machine details to separate package
thecrypticace Jun 30, 2022
11a4014
Update tests
thecrypticace Jun 30, 2022
b92b09b
Fix tests to properly model transition events
thecrypticace Jun 30, 2022
a573a01
WIP
thecrypticace Jun 30, 2022
db60a65
Merge branch 'main' into transition-improvements
thecrypticace Aug 18, 2022
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
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,15 @@
"packages/*"
],
"scripts": {
"core": "yarn workspace @headlessui/core",
"react": "yarn workspace @headlessui/react",
"react-playground": "yarn workspace playground-react dev",
"playground-react": "yarn workspace playground-react dev",
"vue": "yarn workspace @headlessui/vue",
"playground-vue": "yarn workspace playground-vue dev",
"vue-playground": "yarn workspace playground-vue dev",
"clean": "yarn workspaces run clean",
"build": "npm-run-all -p 'react build' 'vue build'",
"build": "npm-run-all -p 'core build' 'react build' 'vue build'",
"test": "./scripts/test.sh",
"lint": "./scripts/lint.sh",
"lint-check": "CI=true ./scripts/lint.sh"
Expand Down
7 changes: 7 additions & 0 deletions packages/@headlessui-core/build/index.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
'use strict'

if (process.env.NODE_ENV === 'production') {
module.exports = require('./headlessui.prod.cjs')
} else {
module.exports = require('./headlessui.dev.cjs')
}
2 changes: 2 additions & 0 deletions packages/@headlessui-core/jest.config.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
let create = require('../../jest/create-jest-config.cjs')
module.exports = create(__dirname, { displayName: ' Vue ' })
39 changes: 39 additions & 0 deletions packages/@headlessui-core/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
{
"name": "@headlessui/core",
"version": "1.6.4",
"description": "A set of internal, shared utilities used by Headless UI",
"main": "dist/index.cjs",
"typings": "dist/index.d.ts",
"module": "dist/headlessui.esm.js",
"license": "MIT",
"files": [
"README.md",
"dist"
],
"exports": {
"import": "./dist/headlessui.esm.js",
"require": "./dist/index.cjs",
"types": "./dist/index.d.ts"
},
"type": "module",
"sideEffects": false,
"engines": {
"node": ">=10"
},
"repository": {
"type": "git",
"url": "git+https://github.com/tailwindlabs/headlessui.git",
"directory": "packages/@headlessui-core"
},
"publishConfig": {
"access": "public"
},
"scripts": {
"prepublishOnly": "npm run build",
"build": "../../scripts/build.sh",
"watch": "../../scripts/watch.sh",
"test": "../../scripts/test.sh",
"lint": "../../scripts/lint.sh",
"clean": "rimraf ./dist"
}
}
2 changes: 2 additions & 0 deletions packages/@headlessui-core/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from "./utils/machine";
export * from "./transition/state";
307 changes: 307 additions & 0 deletions packages/@headlessui-core/src/transition/state.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,307 @@
import { createTransitionMachine, TransitionMachine } from './state'

it('entering', () => {
let root = createTestMachine('root')

expect(root.description).toEqual('container:idle, self:idle, children:idle')

root.send('enter')

expect(root.description).toEqual('container:entering, self:idle, children:idle')

root.send('start')

expect(root.description).toEqual('container:entering, self:running, children:idle')

root.send('stop')

expect(root.description).toEqual('container:done, self:idle, children:idle')

expect(logs).toEqual([
"root event: enter",
"root action: start",
"root state: idle,idle,idle -> entering,idle,idle",
"root event: start",
"root state: entering,idle,idle -> entering,running,idle",
"root event: stop",
"root action: stop",
"root state: entering,running,idle -> done,idle,idle",
])
})

it('leaving', () => {
let root = createTestMachine('root')

expect(root.description).toEqual('container:idle, self:idle, children:idle')

root.send('leave')

expect(root.description).toEqual('container:leaving, self:idle, children:idle')

root.send('start')

expect(root.description).toEqual('container:leaving, self:running, children:idle')

root.send('stop')

expect(root.description).toEqual('container:done, self:idle, children:idle')

expect(logs).toEqual([
"root event: leave",
"root action: start",
"root state: idle,idle,idle -> leaving,idle,idle",
"root event: start",
"root state: leaving,idle,idle -> leaving,running,idle",
"root event: stop",
"root action: stop",
"root state: leaving,running,idle -> done,idle,idle",
])
})

it('entering with children', () => {
let root = createTestMachine('root')
let child1 = createTestMachine('child-1')

root.add(child1)

expect(root.description).toEqual('container:idle, self:idle, children:idle')

root.send('enter')
child1.send('enter')

expect(root.description).toEqual('container:entering, self:idle, children:idle')

root.send('start')

expect(root.description).toEqual('container:entering, self:running, children:idle')

root.send('stop')

expect(root.description).toEqual('container:entering, self:waiting_for_children, children:idle')

child1.send('start')

expect(root.description).toEqual('container:entering, self:waiting_for_children, children:all_running')

child1.send('stop')

expect(root.description).toEqual('container:done, self:idle, children:idle')

expect(logs).toEqual([
"root event: #child.add child-1",
"child-1 event: #child.become root",
"root event: enter",
"root state: idle,idle,idle -> entering,idle,idle",
"child-1 event: enter",
"child-1 action: start",
"child-1 state: idle,idle,idle -> entering,idle,idle",
"root event: start",
"root state: entering,idle,idle -> entering,running,idle",
"root event: stop",
"root state: entering,running,idle -> entering,waiting_for_children,idle",
"child-1 event: start",
"root event: #child.start",
"root action: start",
"root state: entering,waiting_for_children,idle -> entering,waiting_for_children,all_running",
"child-1 state: entering,idle,idle -> entering,running,idle",
"child-1 event: stop",
"child-1 action: stop",
"root event: #child.stop",
"root action: stop",
"root state: entering,waiting_for_children,all_running -> done,idle,idle",
"root action: stop",
"child-1 state: entering,running,idle -> done,idle,idle",
])
})

it('leaving with children', () => {
let root = createTestMachine('root')
let child1 = createTestMachine('child-1')

root.add(child1)

expect(root.description).toEqual('container:idle, self:idle, children:idle')

root.send('leave')
child1.send('leave')

expect(root.description).toEqual('container:leaving, self:idle, children:idle')

root.send('start')

expect(root.description).toEqual('container:leaving, self:running, children:idle')

child1.send('start')

expect(root.description).toEqual('container:leaving, self:running, children:all_running')

child1.send('stop')

expect(root.description).toEqual('container:leaving, self:running, children:waiting_for_self')

root.send('stop')

expect(root.description).toEqual('container:done, self:idle, children:idle')
})

it('leaving with children added while waiting for other children', () => {
let root = createTestMachine('root')
let child1 = createTestMachine('child-1')
let child2 = createTestMachine('child-2')

root.add(child1)

expect(root.description).toEqual('container:idle, self:idle, children:idle')

root.send('leave')
child1.send('leave')
child2.send('leave')

expect(root.description).toEqual('container:leaving, self:idle, children:idle')

root.send('start')

expect(root.description).toEqual('container:leaving, self:running, children:idle')

child1.send('start')

root.add(child2)

child1.send('stop')

expect(root.description).toEqual('container:leaving, self:running, children:all_running')

child2.send('start')
child2.send('stop')

expect(root.description).toEqual('container:leaving, self:running, children:waiting_for_self')

root.send('stop')

expect(root.description).toEqual('container:done, self:idle, children:idle')
})

it('waiting on nested children', () => {
let root = createTestMachine('root')
let child1 = createTestMachine('child-1')
let child11 = createTestMachine('child-1-1')

root.add(child1)
child1.add(child11)

expect(root.description).toEqual('container:idle, self:idle, children:idle')

root.send('leave')
child1.send('leave')
child11.send('leave')

expect(root.description).toEqual('container:leaving, self:idle, children:idle')

root.send('start')

expect(root.description).toEqual('container:leaving, self:running, children:idle')

child1.send('start')

expect(root.description).toEqual('container:leaving, self:running, children:all_running')

child11.send('start')
child11.send('stop')

expect(root.description).toEqual('container:leaving, self:running, children:all_running')

child1.send('stop')

expect(root.description).toEqual('container:leaving, self:running, children:waiting_for_self')

root.send('stop')

expect(root.description).toEqual('container:done, self:idle, children:idle')
})

// Really we want to delay the sending of events right?
// Or do we want to delay when the actions run…

it('start events run in parent -> child order', () => {
let root = createTestMachine('root')
let child1 = createTestMachine('child-1')
let child2 = createTestMachine('child-2')

root.add(child1)
root.add(child2)

child1.send('enter')
child2.send('enter')
root.send('enter')

child1.send('start')
child1.send('stop')

root.send('start')
root.send('stop')

child2.send('start')
child2.send('stop')

console.log(logs)

expect(actions).toEqual([
"child-1: start",
"child-2: start",
"root: start",
"child-1: stop",
"child-2: stop",
"root: stop",
])
})

//
// Helpers
//

let logs: string[] = []
let actions: string[] = []

beforeEach(() => {
logs = []
actions = []
})

function createTestMachine(id: string) {
function log(message: string) {
logs.push(`${id} ${message}`)

if (message.includes('action:')) {
actions.push(`${id} ${message}`.replace(' action:', ':'))
}
}

let machine = createTransitionMachine(id, {
onStart: () => log(`action: start`),
onStop: () => log(`action: stop`),
onCancel: () => log(`action: cancel`),

onChange: (previous, current) =>
log(`state: ${previous.toString()} -> ${current.toString()}`.trim()),

onEvent: (event, payload) =>
log(`event: ${event} ${payload?.id ?? payload ?? ''}`.trim()),
})

Object.defineProperties(machine, {
id: { get: () => id },
logs: { get: () => [...logs] },
description: {
get: () =>
`container:${machine.state[0]}, self:${machine.state[1]}, children:${machine.state[2]}`,
},
})

interface TestMachine extends TransitionMachine {
readonly id: string
readonly logs: readonly string[]
readonly description: string
}

return machine as TestMachine
}
Loading