Add transition prop to DialogPanel and DialogBackdrop components (#3309)
* add internal `ResetOpenClosedProvider`
This will allow us to reset the `OpenClosedProvider` and reset the
"boundary". This is important when we want to wrap a `Dialog` inside of
a `Transition` that exists in another component that is wrapped in a
transition itself.
This will be used in let's say a `DisclosurePanel`:
```tsx
<Disclosure> // OpenClosedProvider
<Transition>
<DisclosurePanel> // ResetOpenClosedProvider
<Dialog /> // Can safely wrap `<Dialog />` in `<Transition />`
</DisclosurePanel>
</Transition>
</Disclosure>
```
* use `ResetOpenClosedProvider` in `PopoverPanel` and `DisclosurePanel`
* add `transition` prop to `<Transition>` component
This prop allows us to enabled / disable the `Transition` functionality.
E.g.: expose the underlying data attributes.
But it will still setup a `Transition` boundary for coordinating the
`TransitionChild` components.
* always wrap `Dialog` in a `Transition` component
+ add `transition` props to the `Dialog`, `DialogPanel` and `DialogBackdrop`
This will allow us individually control the transition on each element,
but also setup the transition boundary on the `Dialog` for coordination
purposes.
* improve dialog playground example
* update built in transition playground example to use individual transition props
* speedup example transitions
* Add validations to DialogFn
This technically means most or all of them can be removed from InternalDialog but we can do that later
* Pass `unmount={false}` from the Dialog to the wrapping transition
* Only wrap Dialog in a Transition if it’s not `static`
I’m not 100% sure this is right but it seems like it might be given that `static` implies it’s always rendered.
* remove validations from `InternalDialog`
Already validated by `Dialog` itself
* use existing `usesOpenClosedState`
* reword comment
* remove flawed test
The reason this test is flawed and why it's safe to delete it:
This test opened the dialog, then clicked on an element outside of the
dialog to close it and prove that we correctly focused that new element
instead of going to the button that opened the dialog in the first
place.
This test used to work before marked the rest of the page as `inert`.
Right now we mark the rest of the page as `inert`, so running this in a
real browser means that we can't click or focus an element outside of
the `dialog` simply because the rest of the page is inert.
The reason it fails all of a sudden is that the introduction of
`<Transition>` around the `<Dialog>` by default purely delays the
mounting just enough to record different elements to try and restore
focus to.
That said, this test clicked outside of a dialog and focused that
element which can't work in a real browser because the element can't be
interacted with at all.
* update changelog
---------
Co-authored-by: Jordan Pittman <jordan@cryptica.me>
This commit is contained in:
@@ -11,7 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
- Add ability to render multiple `Dialog` components at once (without nesting them) ([#3242](https://github.com/tailwindlabs/headlessui/pull/3242))
|
||||
- Add CSS based transitions using `data-*` attributes ([#3273](https://github.com/tailwindlabs/headlessui/pull/3273), [#3285](https://github.com/tailwindlabs/headlessui/pull/3285))
|
||||
- Add `transition` prop to `Dialog` component ([#3307](https://github.com/tailwindlabs/headlessui/pull/3307))
|
||||
- Add `transition` prop to `Dialog`, `DialogBackdrop` and `DialogPanel` components ([#3307](https://github.com/tailwindlabs/headlessui/pull/3307), [#3309](https://github.com/tailwindlabs/headlessui/pull/3309))
|
||||
- Add `DialogBackdrop` component ([#3307](https://github.com/tailwindlabs/headlessui/pull/3307))
|
||||
- Add `PopoverBackdrop` component to replace `PopoverOverlay` ([#3308](https://github.com/tailwindlabs/headlessui/pull/3308))
|
||||
|
||||
|
||||
@@ -1019,41 +1019,6 @@ describe('Mouse interactions', () => {
|
||||
})
|
||||
)
|
||||
|
||||
it(
|
||||
'should be possible to close the dialog, and keep focus on the focusable element',
|
||||
suppressConsoleLogs(async () => {
|
||||
function Example() {
|
||||
let [isOpen, setIsOpen] = useState(false)
|
||||
return (
|
||||
<>
|
||||
<button>Hello</button>
|
||||
<button onClick={() => setIsOpen((v) => !v)}>Trigger</button>
|
||||
<Dialog autoFocus={false} open={isOpen} onClose={setIsOpen}>
|
||||
Contents
|
||||
<TabSentinel />
|
||||
</Dialog>
|
||||
</>
|
||||
)
|
||||
}
|
||||
render(<Example />)
|
||||
|
||||
// Open dialog
|
||||
await click(getByText('Trigger'))
|
||||
|
||||
// Verify it is open
|
||||
assertDialog({ state: DialogState.Visible })
|
||||
|
||||
// Click the button to close (outside click)
|
||||
await click(getByText('Hello'))
|
||||
|
||||
// Verify it is closed
|
||||
assertDialog({ state: DialogState.InvisibleUnmounted })
|
||||
|
||||
// Verify the button is focused
|
||||
assertActiveElement(getByText('Hello'))
|
||||
})
|
||||
)
|
||||
|
||||
it(
|
||||
'should be possible to submit a form inside a Dialog',
|
||||
suppressConsoleLogs(async () => {
|
||||
|
||||
@@ -31,7 +31,7 @@ import { useServerHandoffComplete } from '../../hooks/use-server-handoff-complet
|
||||
import { useSyncRefs } from '../../hooks/use-sync-refs'
|
||||
import { CloseProvider } from '../../internal/close-provider'
|
||||
import { HoistFormFields } from '../../internal/form-fields'
|
||||
import { State, useOpenClosed } from '../../internal/open-closed'
|
||||
import { ResetOpenClosedProvider, State, useOpenClosed } from '../../internal/open-closed'
|
||||
import { ForcePortalRoot } from '../../internal/portal-force-root'
|
||||
import type { Props } from '../../types'
|
||||
import { match } from '../../utils/match'
|
||||
@@ -52,8 +52,6 @@ import { FocusTrap, FocusTrapFeatures } from '../focus-trap/focus-trap'
|
||||
import { Portal, PortalGroup, useNestedPortals } from '../portal/portal'
|
||||
import { Transition, TransitionChild } from '../transition/transition'
|
||||
|
||||
let WithTransitionWrapper = createContext(false)
|
||||
|
||||
enum DialogStates {
|
||||
Open,
|
||||
Closed,
|
||||
@@ -111,33 +109,9 @@ function stateReducer(state: StateDefinition, action: Actions) {
|
||||
|
||||
// ---
|
||||
|
||||
let DEFAULT_DIALOG_TAG = 'div' as const
|
||||
type DialogRenderPropArg = {
|
||||
open: boolean
|
||||
}
|
||||
type DialogPropsWeControl = 'aria-describedby' | 'aria-labelledby' | 'aria-modal'
|
||||
|
||||
let DialogRenderFeatures = RenderFeatures.RenderStrategy | RenderFeatures.Static
|
||||
|
||||
export type DialogProps<TTag extends ElementType = typeof DEFAULT_DIALOG_TAG> = Props<
|
||||
TTag,
|
||||
DialogRenderPropArg,
|
||||
DialogPropsWeControl,
|
||||
PropsForFeatures<typeof DialogRenderFeatures> & {
|
||||
open?: boolean
|
||||
onClose(value: boolean): void
|
||||
initialFocus?: MutableRefObject<HTMLElement | null>
|
||||
role?: 'dialog' | 'alertdialog'
|
||||
autoFocus?: boolean
|
||||
__demoMode?: boolean
|
||||
transition?: boolean
|
||||
}
|
||||
>
|
||||
|
||||
function DialogFn<TTag extends ElementType = typeof DEFAULT_DIALOG_TAG>(
|
||||
props: DialogProps<TTag>,
|
||||
ref: Ref<HTMLElement>
|
||||
) {
|
||||
let InternalDialog = forwardRefWithAs(function InternalDialog<
|
||||
TTag extends ElementType = typeof DEFAULT_DIALOG_TAG,
|
||||
>(props: DialogProps<TTag>, ref: Ref<HTMLElement>) {
|
||||
let internalId = useId()
|
||||
let {
|
||||
id = `headlessui-dialog-${internalId}`,
|
||||
@@ -146,7 +120,6 @@ function DialogFn<TTag extends ElementType = typeof DEFAULT_DIALOG_TAG>(
|
||||
initialFocus,
|
||||
role = 'dialog',
|
||||
autoFocus = true,
|
||||
transition = false,
|
||||
__demoMode = false,
|
||||
...theirProps
|
||||
} = props
|
||||
@@ -179,39 +152,6 @@ function DialogFn<TTag extends ElementType = typeof DEFAULT_DIALOG_TAG>(
|
||||
|
||||
let ownerDocument = useOwnerDocument(internalDialogRef)
|
||||
|
||||
// Validations
|
||||
let hasOpen = props.hasOwnProperty('open') || usesOpenClosedState !== null
|
||||
let hasOnClose = props.hasOwnProperty('onClose')
|
||||
if (!hasOpen && !hasOnClose) {
|
||||
throw new Error(
|
||||
`You have to provide an \`open\` and an \`onClose\` prop to the \`Dialog\` component.`
|
||||
)
|
||||
}
|
||||
|
||||
if (!hasOpen) {
|
||||
throw new Error(
|
||||
`You provided an \`onClose\` prop to the \`Dialog\`, but forgot an \`open\` prop.`
|
||||
)
|
||||
}
|
||||
|
||||
if (!hasOnClose) {
|
||||
throw new Error(
|
||||
`You provided an \`open\` prop to the \`Dialog\`, but forgot an \`onClose\` prop.`
|
||||
)
|
||||
}
|
||||
|
||||
if (typeof open !== 'boolean') {
|
||||
throw new Error(
|
||||
`You provided an \`open\` prop to the \`Dialog\`, but the value is not a boolean. Received: ${open}`
|
||||
)
|
||||
}
|
||||
|
||||
if (typeof onClose !== 'function') {
|
||||
throw new Error(
|
||||
`You provided an \`onClose\` prop to the \`Dialog\`, but the value is not a function. Received: ${onClose}`
|
||||
)
|
||||
}
|
||||
|
||||
let dialogState = open ? DialogStates.Open : DialogStates.Closed
|
||||
|
||||
let [state, dispatch] = useReducer(stateReducer, {
|
||||
@@ -343,19 +283,8 @@ function DialogFn<TTag extends ElementType = typeof DEFAULT_DIALOG_TAG>(
|
||||
}
|
||||
}
|
||||
|
||||
if (transition) {
|
||||
let { transition: _transition, open, ...rest } = props
|
||||
return (
|
||||
<WithTransitionWrapper.Provider value={true}>
|
||||
<Transition show={open}>
|
||||
<Dialog ref={ref} {...rest} />
|
||||
</Transition>
|
||||
</WithTransitionWrapper.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<ResetOpenClosedProvider>
|
||||
<ForcePortalRoot force={true}>
|
||||
<Portal>
|
||||
<DialogContext.Provider value={contextBag}>
|
||||
@@ -391,8 +320,86 @@ function DialogFn<TTag extends ElementType = typeof DEFAULT_DIALOG_TAG>(
|
||||
<HoistFormFields>
|
||||
<MainTreeNode />
|
||||
</HoistFormFields>
|
||||
</>
|
||||
</ResetOpenClosedProvider>
|
||||
)
|
||||
})
|
||||
|
||||
// ---
|
||||
|
||||
let DEFAULT_DIALOG_TAG = 'div' as const
|
||||
type DialogRenderPropArg = {
|
||||
open: boolean
|
||||
}
|
||||
type DialogPropsWeControl = 'aria-describedby' | 'aria-labelledby' | 'aria-modal'
|
||||
|
||||
let DialogRenderFeatures = RenderFeatures.RenderStrategy | RenderFeatures.Static
|
||||
|
||||
export type DialogProps<TTag extends ElementType = typeof DEFAULT_DIALOG_TAG> = Props<
|
||||
TTag,
|
||||
DialogRenderPropArg,
|
||||
DialogPropsWeControl,
|
||||
PropsForFeatures<typeof DialogRenderFeatures> & {
|
||||
open?: boolean
|
||||
onClose(value: boolean): void
|
||||
initialFocus?: MutableRefObject<HTMLElement | null>
|
||||
role?: 'dialog' | 'alertdialog'
|
||||
autoFocus?: boolean
|
||||
transition?: boolean
|
||||
__demoMode?: boolean
|
||||
}
|
||||
>
|
||||
|
||||
function DialogFn<TTag extends ElementType = typeof DEFAULT_DIALOG_TAG>(
|
||||
props: DialogProps<TTag>,
|
||||
ref: Ref<HTMLElement>
|
||||
) {
|
||||
let { transition = false, open, ...rest } = props
|
||||
|
||||
// Validations
|
||||
let usesOpenClosedState = useOpenClosed()
|
||||
let hasOpen = props.hasOwnProperty('open') || usesOpenClosedState !== null
|
||||
let hasOnClose = props.hasOwnProperty('onClose')
|
||||
|
||||
if (!hasOpen && !hasOnClose) {
|
||||
throw new Error(
|
||||
`You have to provide an \`open\` and an \`onClose\` prop to the \`Dialog\` component.`
|
||||
)
|
||||
}
|
||||
|
||||
if (!hasOpen) {
|
||||
throw new Error(
|
||||
`You provided an \`onClose\` prop to the \`Dialog\`, but forgot an \`open\` prop.`
|
||||
)
|
||||
}
|
||||
|
||||
if (!hasOnClose) {
|
||||
throw new Error(
|
||||
`You provided an \`open\` prop to the \`Dialog\`, but forgot an \`onClose\` prop.`
|
||||
)
|
||||
}
|
||||
|
||||
if (!usesOpenClosedState && typeof props.open !== 'boolean') {
|
||||
throw new Error(
|
||||
`You provided an \`open\` prop to the \`Dialog\`, but the value is not a boolean. Received: ${props.open}`
|
||||
)
|
||||
}
|
||||
|
||||
if (typeof props.onClose !== 'function') {
|
||||
throw new Error(
|
||||
`You provided an \`onClose\` prop to the \`Dialog\`, but the value is not a function. Received: ${props.onClose}`
|
||||
)
|
||||
}
|
||||
|
||||
let inTransitionComponent = usesOpenClosedState !== null
|
||||
if (!inTransitionComponent && open !== undefined && !rest.static) {
|
||||
return (
|
||||
<Transition show={open} transition={transition} unmount={rest.unmount}>
|
||||
<InternalDialog ref={ref} {...rest} />
|
||||
</Transition>
|
||||
)
|
||||
}
|
||||
|
||||
return <InternalDialog ref={ref} open={open} {...rest} />
|
||||
}
|
||||
|
||||
// ---
|
||||
@@ -404,7 +411,9 @@ type PanelRenderPropArg = {
|
||||
|
||||
export type DialogPanelProps<TTag extends ElementType = typeof DEFAULT_PANEL_TAG> = Props<
|
||||
TTag,
|
||||
PanelRenderPropArg
|
||||
PanelRenderPropArg,
|
||||
never,
|
||||
{ transition?: boolean }
|
||||
>
|
||||
|
||||
function PanelFn<TTag extends ElementType = typeof DEFAULT_PANEL_TAG>(
|
||||
@@ -412,7 +421,7 @@ function PanelFn<TTag extends ElementType = typeof DEFAULT_PANEL_TAG>(
|
||||
ref: Ref<HTMLElement>
|
||||
) {
|
||||
let internalId = useId()
|
||||
let { id = `headlessui-dialog-panel-${internalId}`, ...theirProps } = props
|
||||
let { id = `headlessui-dialog-panel-${internalId}`, transition = false, ...theirProps } = props
|
||||
let [{ dialogState }, state] = useDialogContext('Dialog.Panel')
|
||||
let panelRef = useSyncRefs(ref, state.panelRef)
|
||||
|
||||
@@ -433,20 +442,18 @@ function PanelFn<TTag extends ElementType = typeof DEFAULT_PANEL_TAG>(
|
||||
onClick: handleClick,
|
||||
}
|
||||
|
||||
let Wrapper = useContext(WithTransitionWrapper) ? TransitionChild : Fragment
|
||||
let Wrapper = transition ? TransitionChild : Fragment
|
||||
|
||||
return (
|
||||
<WithTransitionWrapper.Provider value={false}>
|
||||
<Wrapper>
|
||||
{render({
|
||||
ourProps,
|
||||
theirProps,
|
||||
slot,
|
||||
defaultTag: DEFAULT_PANEL_TAG,
|
||||
name: 'Dialog.Panel',
|
||||
})}
|
||||
</Wrapper>
|
||||
</WithTransitionWrapper.Provider>
|
||||
<Wrapper>
|
||||
{render({
|
||||
ourProps,
|
||||
theirProps,
|
||||
slot,
|
||||
defaultTag: DEFAULT_PANEL_TAG,
|
||||
name: 'Dialog.Panel',
|
||||
})}
|
||||
</Wrapper>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -459,14 +466,16 @@ type BackdropRenderPropArg = {
|
||||
|
||||
export type DialogBackdropProps<TTag extends ElementType = typeof DEFAULT_BACKDROP_TAG> = Props<
|
||||
TTag,
|
||||
BackdropRenderPropArg
|
||||
BackdropRenderPropArg,
|
||||
never,
|
||||
{ transition?: boolean }
|
||||
>
|
||||
|
||||
function BackdropFn<TTag extends ElementType = typeof DEFAULT_BACKDROP_TAG>(
|
||||
props: DialogBackdropProps<TTag>,
|
||||
ref: Ref<HTMLElement>
|
||||
) {
|
||||
let theirProps = props
|
||||
let { transition = false, ...theirProps } = props
|
||||
let [{ dialogState }] = useDialogContext('Dialog.Backdrop')
|
||||
|
||||
let slot = useMemo(
|
||||
@@ -476,7 +485,7 @@ function BackdropFn<TTag extends ElementType = typeof DEFAULT_BACKDROP_TAG>(
|
||||
|
||||
let ourProps = { ref }
|
||||
|
||||
let Wrapper = useContext(WithTransitionWrapper) ? TransitionChild : Fragment
|
||||
let Wrapper = transition ? TransitionChild : Fragment
|
||||
|
||||
return (
|
||||
<Wrapper>
|
||||
|
||||
@@ -26,7 +26,12 @@ import { useResolveButtonType } from '../../hooks/use-resolve-button-type'
|
||||
import { optionalRef, useSyncRefs } from '../../hooks/use-sync-refs'
|
||||
import { useTransition, type TransitionData } from '../../hooks/use-transition'
|
||||
import { CloseProvider } from '../../internal/close-provider'
|
||||
import { OpenClosedProvider, State, useOpenClosed } from '../../internal/open-closed'
|
||||
import {
|
||||
OpenClosedProvider,
|
||||
ResetOpenClosedProvider,
|
||||
State,
|
||||
useOpenClosed,
|
||||
} from '../../internal/open-closed'
|
||||
import type { Props } from '../../types'
|
||||
import { isDisabledReactIssue7711 } from '../../utils/bugs'
|
||||
import { match } from '../../utils/match'
|
||||
@@ -480,18 +485,20 @@ function PanelFn<TTag extends ElementType = typeof DEFAULT_PANEL_TAG>(
|
||||
}
|
||||
|
||||
return (
|
||||
<DisclosurePanelContext.Provider value={state.panelId}>
|
||||
{render({
|
||||
mergeRefs,
|
||||
ourProps,
|
||||
theirProps,
|
||||
slot,
|
||||
defaultTag: DEFAULT_PANEL_TAG,
|
||||
features: PanelRenderFeatures,
|
||||
visible,
|
||||
name: 'Disclosure.Panel',
|
||||
})}
|
||||
</DisclosurePanelContext.Provider>
|
||||
<ResetOpenClosedProvider>
|
||||
<DisclosurePanelContext.Provider value={state.panelId}>
|
||||
{render({
|
||||
mergeRefs,
|
||||
ourProps,
|
||||
theirProps,
|
||||
slot,
|
||||
defaultTag: DEFAULT_PANEL_TAG,
|
||||
features: PanelRenderFeatures,
|
||||
visible,
|
||||
name: 'Disclosure.Panel',
|
||||
})}
|
||||
</DisclosurePanelContext.Provider>
|
||||
</ResetOpenClosedProvider>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -47,7 +47,12 @@ import {
|
||||
type AnchorProps,
|
||||
} from '../../internal/floating'
|
||||
import { Hidden, HiddenFeatures } from '../../internal/hidden'
|
||||
import { OpenClosedProvider, State, useOpenClosed } from '../../internal/open-closed'
|
||||
import {
|
||||
OpenClosedProvider,
|
||||
ResetOpenClosedProvider,
|
||||
State,
|
||||
useOpenClosed,
|
||||
} from '../../internal/open-closed'
|
||||
import type { Props } from '../../types'
|
||||
import { isDisabledReactIssue7711 } from '../../utils/bugs'
|
||||
import {
|
||||
@@ -1043,44 +1048,46 @@ function PanelFn<TTag extends ElementType = typeof DEFAULT_PANEL_TAG>(
|
||||
})
|
||||
|
||||
return (
|
||||
<PopoverPanelContext.Provider value={id}>
|
||||
<PopoverAPIContext.Provider value={{ close, isPortalled }}>
|
||||
<Portal enabled={portal ? props.static || visible : false}>
|
||||
{visible && isPortalled && (
|
||||
<Hidden
|
||||
id={beforePanelSentinelId}
|
||||
ref={state.beforePanelSentinel}
|
||||
features={HiddenFeatures.Focusable}
|
||||
data-headlessui-focus-guard
|
||||
as="button"
|
||||
type="button"
|
||||
onFocus={handleBeforeFocus}
|
||||
/>
|
||||
)}
|
||||
{render({
|
||||
mergeRefs,
|
||||
ourProps,
|
||||
theirProps,
|
||||
slot,
|
||||
defaultTag: DEFAULT_PANEL_TAG,
|
||||
features: PanelRenderFeatures,
|
||||
visible,
|
||||
name: 'Popover.Panel',
|
||||
})}
|
||||
{visible && isPortalled && (
|
||||
<Hidden
|
||||
id={afterPanelSentinelId}
|
||||
ref={state.afterPanelSentinel}
|
||||
features={HiddenFeatures.Focusable}
|
||||
data-headlessui-focus-guard
|
||||
as="button"
|
||||
type="button"
|
||||
onFocus={handleAfterFocus}
|
||||
/>
|
||||
)}
|
||||
</Portal>
|
||||
</PopoverAPIContext.Provider>
|
||||
</PopoverPanelContext.Provider>
|
||||
<ResetOpenClosedProvider>
|
||||
<PopoverPanelContext.Provider value={id}>
|
||||
<PopoverAPIContext.Provider value={{ close, isPortalled }}>
|
||||
<Portal enabled={portal ? props.static || visible : false}>
|
||||
{visible && isPortalled && (
|
||||
<Hidden
|
||||
id={beforePanelSentinelId}
|
||||
ref={state.beforePanelSentinel}
|
||||
features={HiddenFeatures.Focusable}
|
||||
data-headlessui-focus-guard
|
||||
as="button"
|
||||
type="button"
|
||||
onFocus={handleBeforeFocus}
|
||||
/>
|
||||
)}
|
||||
{render({
|
||||
mergeRefs,
|
||||
ourProps,
|
||||
theirProps,
|
||||
slot,
|
||||
defaultTag: DEFAULT_PANEL_TAG,
|
||||
features: PanelRenderFeatures,
|
||||
visible,
|
||||
name: 'Popover.Panel',
|
||||
})}
|
||||
{visible && isPortalled && (
|
||||
<Hidden
|
||||
id={afterPanelSentinelId}
|
||||
ref={state.afterPanelSentinel}
|
||||
features={HiddenFeatures.Focusable}
|
||||
data-headlessui-focus-guard
|
||||
as="button"
|
||||
type="button"
|
||||
onFocus={handleAfterFocus}
|
||||
/>
|
||||
)}
|
||||
</Portal>
|
||||
</PopoverAPIContext.Provider>
|
||||
</PopoverPanelContext.Provider>
|
||||
</ResetOpenClosedProvider>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -119,7 +119,7 @@ export type TransitionChildProps<TTag extends ReactTag> = Props<
|
||||
TransitionChildPropsWeControl,
|
||||
PropsForFeatures<typeof TransitionChildRenderFeatures> &
|
||||
TransitionClasses &
|
||||
TransitionEvents & { appear?: boolean }
|
||||
TransitionEvents & { transition?: boolean; appear?: boolean }
|
||||
>
|
||||
|
||||
function useTransitionContext() {
|
||||
@@ -298,6 +298,11 @@ function TransitionChildFn<TTag extends ElementType = typeof DEFAULT_TRANSITION_
|
||||
ref: Ref<HTMLElement>
|
||||
) {
|
||||
let {
|
||||
// Wether or not to enable transitions on the current element (by exposing
|
||||
// transition data). When set to false, the `Transition` component still
|
||||
// acts as a transition boundary for `TransitionChild` components.
|
||||
transition = true,
|
||||
|
||||
// Event "handlers"
|
||||
beforeEnter,
|
||||
afterEnter,
|
||||
@@ -402,15 +407,19 @@ function TransitionChildFn<TTag extends ElementType = typeof DEFAULT_TRANSITION_
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
if (requiresRef) return
|
||||
if (requiresRef && transition) return
|
||||
|
||||
// When we don't transition, then we can complete the transition
|
||||
// immediately.
|
||||
start(show)
|
||||
end(show)
|
||||
}, [show, requiresRef])
|
||||
}, [show, requiresRef, transition])
|
||||
|
||||
let enabled = (() => {
|
||||
// Should the current component transition? If not, then we can still
|
||||
// orchestrate the child transitions.
|
||||
if (!transition) return false
|
||||
|
||||
// If we don't require a ref, then we can't transition.
|
||||
if (!requiresRef) return false
|
||||
|
||||
|
||||
@@ -22,3 +22,7 @@ interface Props {
|
||||
export function OpenClosedProvider({ value, children }: Props): ReactElement {
|
||||
return <Context.Provider value={value}>{children}</Context.Provider>
|
||||
}
|
||||
|
||||
export function ResetOpenClosedProvider({ children }: { children: React.ReactNode }): ReactElement {
|
||||
return <Context.Provider value={null}>{children}</Context.Provider>
|
||||
}
|
||||
|
||||
@@ -5,18 +5,28 @@ import { classNames } from '../../utils/class-names'
|
||||
|
||||
export default function Home() {
|
||||
let [isOpen, setIsOpen] = useState(false)
|
||||
let [transition, setTransition] = useState(true)
|
||||
let [transitionBackdrop, setTransitionBackdrop] = useState(true)
|
||||
let [transitionPanel, setTransitionPanel] = useState(true)
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex gap-4 p-12">
|
||||
<Button onClick={() => setIsOpen((v) => !v)}>Toggle!</Button>
|
||||
<Button onClick={() => setTransition((v) => !v)}>
|
||||
<span>Toggle transition</span>
|
||||
<Button onClick={() => setTransitionBackdrop((v) => !v)}>
|
||||
<span>Toggle transition backdrop</span>
|
||||
<span
|
||||
className={classNames(
|
||||
'ml-2 inline-flex size-4 rounded-md',
|
||||
transition ? 'bg-green-500' : 'bg-red-500'
|
||||
transitionBackdrop ? 'bg-green-500' : 'bg-red-500'
|
||||
)}
|
||||
></span>
|
||||
</Button>
|
||||
<Button onClick={() => setTransitionPanel((v) => !v)}>
|
||||
<span>Toggle transition panel</span>
|
||||
<span
|
||||
className={classNames(
|
||||
'ml-2 inline-flex size-4 rounded-md',
|
||||
transitionPanel ? 'bg-green-500' : 'bg-red-500'
|
||||
)}
|
||||
></span>
|
||||
</Button>
|
||||
@@ -24,13 +34,18 @@ export default function Home() {
|
||||
|
||||
<Dialog
|
||||
open={isOpen}
|
||||
transition={transition}
|
||||
onClose={() => setIsOpen(false)}
|
||||
className="relative z-50"
|
||||
className="relative z-50 duration-500 data-[closed]:opacity-0"
|
||||
>
|
||||
<DialogBackdrop className="fixed inset-0 bg-black/30 duration-500 ease-out data-[closed]:opacity-0" />
|
||||
<DialogBackdrop
|
||||
transition={transitionBackdrop}
|
||||
className="fixed inset-0 bg-black/30 duration-500 ease-out data-[closed]:opacity-0"
|
||||
/>
|
||||
<div className="fixed inset-0 flex w-screen items-center justify-center p-4">
|
||||
<DialogPanel className="w-full max-w-lg space-y-4 bg-white p-12 duration-500 ease-out data-[closed]:scale-95 data-[closed]:opacity-0">
|
||||
<DialogPanel
|
||||
transition={transitionPanel}
|
||||
className="w-full max-w-lg space-y-4 bg-white p-12 duration-500 ease-out data-[closed]:scale-95 data-[closed]:opacity-0"
|
||||
>
|
||||
<h1 className="text-2xl font-bold">Dialog</h1>
|
||||
<p>
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed pulvinar, nunc nec
|
||||
|
||||
@@ -80,17 +80,17 @@ export default function Home() {
|
||||
<Transition.Child
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0"
|
||||
enterTo="opacity-75"
|
||||
enterTo="opacity-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-75"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
entered="opacity-75"
|
||||
entered="opacity-100"
|
||||
beforeEnter={() => console.log('[Transition.Child] [Overlay] Before enter')}
|
||||
afterEnter={() => console.log('[Transition.Child] [Overlay] After enter')}
|
||||
beforeLeave={() => console.log('[Transition.Child] [Overlay] Before leave')}
|
||||
afterLeave={() => console.log('[Transition.Child] [Overlay] After leave')}
|
||||
>
|
||||
<div className="fixed inset-0 bg-gray-500 transition-opacity" />
|
||||
<div className="fixed inset-0 bg-gray-500/75 transition-opacity" />
|
||||
</Transition.Child>
|
||||
|
||||
<Transition.Child
|
||||
|
||||
Reference in New Issue
Block a user