Skip to content

Commit

Permalink
Ensure that you can use Transition Child components in more scenario's (
Browse files Browse the repository at this point in the history
#503)

* ensure that you can use Transition Child components

When you are using the implicit variants of the components, for example
when you are using a Transition component inside a Menu component then
it might look weird in Vue.

The Vue code could look like this:

```
<Menu>
  <TransitionRoot>
    <MenuItems>...</MenuItems>
  </TransitionRoot>
<Menu>
```

However, `TransitionRoot` doesn't make much sense here because it sits
in the middle of 2 components, and it is also not controlled by an
explicit `show` prop.

This commit will allows you to use a `TransitionChild` instead (in fact,
both work).

We basically now do a few things, when you are using a TransitionChild:

- Do we have a parent `TransitionRoot`? Yes -> Use it
- Do we have an open closed state? Yes -> Render a TransitionRoot in
  between behind the scenes.
- Throw the error we were throwing before!

* update changelog
  • Loading branch information
RobinMalfait committed May 10, 2021
1 parent a8bbd0e commit e40c66c
Show file tree
Hide file tree
Showing 6 changed files with 177 additions and 6 deletions.
8 changes: 6 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased - React]

- Nothing yet!
### Added

- Ensure that you can use `Transition.Child` when using implicit Transitions ([#503](https://github.com/tailwindlabs/headlessui/pull/503))

## [Unreleased - Vue]

- Nothing yet!
### Added

- Ensure that you can use `TransitionChild` when using implicit Transitions ([#503](https://github.com/tailwindlabs/headlessui/pull/503))

## [@headlessui/react@v1.2.0] - 2021-05-10

Expand Down
54 changes: 54 additions & 0 deletions packages/@headlessui-react/src/components/menu/menu.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -496,6 +496,60 @@ describe('Composition', () => {
])
})
)

it(
'should be possible to wrap the Menu.Items with a Transition.Child component',
suppressConsoleLogs(async () => {
let orderFn = jest.fn()
render(
<Menu>
<Menu.Button>Trigger</Menu.Button>
<Debug name="Menu" fn={orderFn} />
<Transition.Child>
<Debug name="Transition" fn={orderFn} />
<Menu.Items>
<Menu.Item as="a">
{data => (
<>
{JSON.stringify(data)}
<Debug name="Menu.Item" fn={orderFn} />
</>
)}
</Menu.Item>
</Menu.Items>
</Transition.Child>
</Menu>
)

assertMenuButton({
state: MenuState.InvisibleUnmounted,
attributes: { id: 'headlessui-menu-button-1' },
})
assertMenu({ state: MenuState.InvisibleUnmounted })

await click(getMenuButton())

assertMenuButton({
state: MenuState.Visible,
attributes: { id: 'headlessui-menu-button-1' },
})
assertMenu({
state: MenuState.Visible,
textContent: JSON.stringify({ active: false, disabled: false }),
})

await click(getMenuButton())

// Verify that we tracked the `mounts` and `unmounts` in the correct order
expect(orderFn.mock.calls).toEqual([
['Mounting - Menu'],
['Mounting - Transition'],
['Mounting - Menu.Item'],
['Unmounting - Transition'],
['Unmounting - Menu.Item'],
])
})
)
})

describe('Keyboard interactions', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -400,5 +400,16 @@ export function Transition<TTag extends ElementType = typeof DEFAULT_TRANSITION_
)
}

Transition.Child = TransitionChild
Transition.Child = function Child<TTag extends ElementType = typeof DEFAULT_TRANSITION_CHILD_TAG>(
props: TransitionChildProps<TTag>
) {
let hasTransitionContext = useContext(TransitionContext) !== null
let hasOpenClosedContext = useOpenClosed() !== null

return !hasTransitionContext && hasOpenClosedContext ? (
<Transition {...props} />
) : (
<TransitionChild {...props} />
)
}
Transition.Root = Transition
61 changes: 61 additions & 0 deletions packages/@headlessui-vue/src/components/menu/menu.test.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { defineComponent, h, nextTick, ref, watch } from 'vue'
import { render } from '../../test-utils/vue-testing-library'
import { Menu, MenuButton, MenuItems, MenuItem } from './menu'
import { TransitionChild } from '../transitions/transition'
import { suppressConsoleLogs } from '../../test-utils/suppress-console-logs'
import {
MenuState,
Expand Down Expand Up @@ -41,6 +42,16 @@ beforeAll(() => {

afterAll(() => jest.restoreAllMocks())

function nextFrame() {
return new Promise(resolve => {
requestAnimationFrame(() => {
requestAnimationFrame(() => {
resolve()
})
})
})
}

function renderTemplate(input: string | Partial<Parameters<typeof defineComponent>[0]>) {
let defaultComponents = { Menu, MenuButton, MenuItems, MenuItem }

Expand Down Expand Up @@ -880,6 +891,56 @@ describe('Composition', () => {
expect(readFn).toHaveBeenNthCalledWith(3, State.Open)
})
)

it('should be possible to render a TransitionChild that inherits state from the Menu', async () => {
let readFn = jest.fn()
renderTemplate({
components: { TransitionChild },
template: jsx`
<Menu>
<MenuButton>Trigger</MenuButton>
<TransitionChild
as="template"
@beforeEnter="readFn('enter')"
@beforeLeave="readFn('leave')"
>
<MenuItems>
<MenuItem as="button">I am a button</MenuItem>
</MenuItems>
</TransitionChild>
</Menu>
`,
setup() {
return { readFn }
},
})

// Verify the Menu is hidden
assertMenu({ state: MenuState.InvisibleUnmounted })

// Let's toggle the Menu
await click(getMenuButton())

// Verify that our transition fired
expect(readFn).toHaveBeenCalledTimes(1)
expect(readFn).toHaveBeenNthCalledWith(1, 'enter')

// Verify the Menu is visible
assertMenu({ state: MenuState.Visible })

// Let's toggle the Menu
await click(getMenuButton())

// Verify that our transition fired
expect(readFn).toHaveBeenCalledTimes(2)
expect(readFn).toHaveBeenNthCalledWith(2, 'leave')

// Wait for the transitions to finish
await nextFrame()

// Verify the Menu is hidden
assertMenu({ state: MenuState.InvisibleUnmounted })
})
})

describe('Keyboard interactions', () => {
Expand Down
43 changes: 40 additions & 3 deletions packages/@headlessui-vue/src/components/transitions/transition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,12 @@ import { match } from '../../utils/match'
import { Features, render, RenderStrategy } from '../../utils/render'
import { Reason, transition } from './utils/transition'
import { dom } from '../../utils/dom'
import { useOpenClosedProvider, State, useOpenClosed } from '../../internal/open-closed'
import {
useOpenClosedProvider,
State,
useOpenClosed,
hasOpenClosed,
} from '../../internal/open-closed'

type ID = ReturnType<typeof useId>

Expand All @@ -40,6 +45,10 @@ enum TreeStates {
Hidden = 'hidden',
}

function hasTransitionContext() {
return inject(TransitionContext, null) !== null
}

function useTransitionContext() {
let context = inject(TransitionContext, null)

Expand Down Expand Up @@ -137,6 +146,20 @@ export let TransitionChild = defineComponent({
},
emits: ['beforeEnter', 'afterEnter', 'beforeLeave', 'afterLeave'],
render() {
if (this.renderAsRoot) {
return h(
TransitionRoot,
{
...this.$props,
onBeforeEnter: () => this.$emit('beforeEnter'),
onAfterEnter: () => this.$emit('afterEnter'),
onBeforeLeave: () => this.$emit('beforeLeave'),
onAfterLeave: () => this.$emit('afterLeave'),
},
this.$slots
)
}

let {
appear,
show,
Expand Down Expand Up @@ -165,6 +188,12 @@ export let TransitionChild = defineComponent({
})
},
setup(props, { emit }) {
if (!hasTransitionContext() && hasOpenClosed()) {
return {
renderAsRoot: true,
}
}

let container = ref<HTMLElement | null>(null)
let state = ref(TreeStates.Visible)
let strategy = computed(() => (props.unmount ? RenderStrategy.Unmount : RenderStrategy.Hidden))
Expand Down Expand Up @@ -289,7 +318,7 @@ export let TransitionChild = defineComponent({
)
)

return { el: container, state }
return { el: container, renderAsRoot: false, state }
},
})

Expand Down Expand Up @@ -325,7 +354,15 @@ export let TransitionRoot = defineComponent({
default: () => [
h(
TransitionChild,
{ ...this.$attrs, ...sharedProps, ...passThroughProps },
{
onBeforeEnter: () => this.$emit('beforeEnter'),
onAfterEnter: () => this.$emit('afterEnter'),
onBeforeLeave: () => this.$emit('beforeLeave'),
onAfterLeave: () => this.$emit('afterLeave'),
...this.$attrs,
...sharedProps,
...passThroughProps,
},
this.$slots.default
),
],
Expand Down
4 changes: 4 additions & 0 deletions packages/@headlessui-vue/src/internal/open-closed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ export enum State {
Closed,
}

export function hasOpenClosed() {
return useOpenClosed() !== null
}

export function useOpenClosed() {
return inject(Context, null)
}
Expand Down

0 comments on commit e40c66c

Please sign in to comment.