Skip to content

Commit

Permalink
Extract transition component handling to useTransitionComponent hook
Browse files Browse the repository at this point in the history
  • Loading branch information
acelaya committed Jul 25, 2023
1 parent f1aeeb9 commit 55fc983
Show file tree
Hide file tree
Showing 3 changed files with 119 additions and 70 deletions.
84 changes: 14 additions & 70 deletions src/components/feedback/Dialog.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,12 @@
import classnames from 'classnames';
import type { RefObject } from 'preact';
import { Fragment } from 'preact';
import {
useCallback,
useEffect,
useLayoutEffect,
useMemo,
useRef,
useState,
} from 'preact/hooks';
import { useCallback, useLayoutEffect, useRef } from 'preact/hooks';

import { useClickAway } from '../../hooks/use-click-away';
import { useFocusAway } from '../../hooks/use-focus-away';
import { useKeyPress } from '../../hooks/use-key-press';
import { useSyncedRef } from '../../hooks/use-synced-ref';
import { useTransitionComponent } from '../../hooks/use-transition-component';
import { useUniqueId } from '../../hooks/use-unique-id';
import type { PresentationalProps, TransitionComponent } from '../../types';
import { downcastRef } from '../../util/typing';
Expand Down Expand Up @@ -80,7 +73,7 @@ const Dialog = function Dialog({
children,
initialFocus = 'auto',
restoreFocus = false,
transitionComponent: TransitionComponent,
transitionComponent,
variant = 'panel',
closed = false,

Expand All @@ -100,23 +93,6 @@ const Dialog = function Dialog({
const modalRef = useSyncedRef(elementRef);
const restoreFocusEl = useRef<HTMLElement | null>(null);

// TODO To properly handle closing/opening with a TransitionComponent, these
// two pieces of state need to be synced with the `closed` prop.
// That makes the logic hard to follow. It would be good to revisit eventually
const [isClosed, setIsClosed] = useState(closed);
const [transitionComponentVisible, setTransitionComponentVisible] =
useState(false);

const closeHandler = useCallback(() => {
if (TransitionComponent) {
// When a TransitionComponent is provided, the actual "onClose" will be
// called by that component, once the "out" transition has finished
setTransitionComponentVisible(false);
} else {
onClose?.();
}
}, [onClose, TransitionComponent]);

const initializeDialog = useCallback(() => {
if (initialFocus === 'manual') {
return;
Expand All @@ -142,21 +118,21 @@ const Dialog = function Dialog({
}
}, [initialFocus, modalRef]);

const { closeHandler, isClosed, wrapWithTransition } = useTransitionComponent(
{
closed,
transitionComponent,
onClose,
onOpen: initializeDialog,
}
);

const doRestoreFocus = useCallback(() => {
if (restoreFocus) {
restoreFocusEl.current?.focus();
}
}, [restoreFocus]);

const onTransitionEnd = (direction: 'in' | 'out') => {
if (direction === 'in') {
initializeDialog();
} else {
setIsClosed(true);
onClose?.();
}
};

useClickAway(modalRef, closeHandler, {
enabled: closeOnClickAway,
});
Expand All @@ -170,35 +146,6 @@ const Dialog = function Dialog({
});

const dialogDescriptionId = useUniqueId('dialog-description');
const Wrapper = useMemo(
() => TransitionComponent ?? Fragment,
[TransitionComponent]
);

useEffect(() => {
if (closed) {
if (TransitionComponent) {
setTransitionComponentVisible(false);
} else {
setIsClosed(true);
}
} else {
setIsClosed(false);
}

if (!closed && !TransitionComponent) {
initializeDialog();
}

// We only want to run this effect when opened or closed.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [closed]);

useEffect(() => {
if (!isClosed) {
setTransitionComponentVisible(true);
}
}, [isClosed]);

useLayoutEffect(() => {
if (!isClosed) {
Expand Down Expand Up @@ -239,10 +186,7 @@ const Dialog = function Dialog({

return (
<CloseableContext.Provider value={closeableContext}>
<Wrapper
direction={transitionComponentVisible ? 'in' : 'out'}
onTransitionEnd={onTransitionEnd}
>
{wrapWithTransition(
<div
data-component="Dialog"
tabIndex={-1}
Expand Down Expand Up @@ -270,7 +214,7 @@ const Dialog = function Dialog({
)}
{variant === 'custom' && <>{children}</>}
</div>
</Wrapper>
)}
</CloseableContext.Provider>
);
};
Expand Down
104 changes: 104 additions & 0 deletions src/hooks/use-transition-component.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import type { ComponentChildren } from 'preact';
import type { JSX } from 'preact';
import { useCallback, useEffect, useState } from 'preact/hooks';

import type { TransitionComponent } from '../types';

type TransitionComponentOptions = {
closed: boolean;
transitionComponent?: TransitionComponent;
onClose?: () => void;
onOpen?: () => void;
};

type TransitionComponentResult = {
closeHandler: () => void;
isClosed: boolean;
wrapWithTransition: (children: ComponentChildren) => JSX.Element;
};

export function useTransitionComponent({
closed,
transitionComponent,
onClose,
onOpen,
}: TransitionComponentOptions): TransitionComponentResult {
// TODO To properly handle closing/opening with a TransitionComponent, these
// two pieces of state need to be synced with the `closed` argument.
// That makes the logic hard to follow. It would be good to revisit eventually
const [isClosed, setIsClosed] = useState(closed);
const [transitionComponentVisible, setTransitionComponentVisible] =
useState(false);

const closeHandler = useCallback(() => {
if (transitionComponent) {
// When a TransitionComponent is provided, the actual "onClose" will be
// called by that component, once the "out" transition has finished
setTransitionComponentVisible(false);
} else {
onClose?.();
}
}, [onClose, transitionComponent]);

const onTransitionEnd = useCallback(
(direction: 'in' | 'out') => {
if (direction === 'in') {
onOpen?.();
} else {
setIsClosed(true);
onClose?.();
}
},
[onOpen, onClose]
);

useEffect(() => {
if (closed) {
if (transitionComponent) {
setTransitionComponentVisible(false);
} else {
setIsClosed(true);
}
} else {
setIsClosed(false);
}

if (!closed && !transitionComponent) {
onOpen?.();
}

// We only want to run this effect when opened or closed.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [closed]);

useEffect(() => {
if (!isClosed) {
setTransitionComponentVisible(true);
}
}, [isClosed]);

const wrapWithTransition = useCallback(
(children: ComponentChildren) => {
if (!transitionComponent) {
return <>{children}</>;
}

const TransitionComp = transitionComponent;
return (
<TransitionComp
direction={transitionComponentVisible ? 'in' : 'out'}
onTransitionEnd={onTransitionEnd}
>
{children}
</TransitionComp>
);
},
[transitionComponent, transitionComponentVisible, onTransitionEnd]
);

return {
closeHandler,
isClosed,
wrapWithTransition,
};
}
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export { useClickAway } from './hooks/use-click-away';
export { useFocusAway } from './hooks/use-focus-away';
export { useKeyPress } from './hooks/use-key-press';
export { useSyncedRef } from './hooks/use-synced-ref';
export { useTransitionComponent } from './hooks/use-transition-component';

// Components
export * from './components/icons';
Expand Down

0 comments on commit 55fc983

Please sign in to comment.