Skip to content

Commit

Permalink
feat(GamesTable): Animate row height when filtering
Browse files Browse the repository at this point in the history
  • Loading branch information
PrettyCoffee committed Apr 4, 2024
1 parent 94d7578 commit 8827d8a
Show file tree
Hide file tree
Showing 3 changed files with 192 additions and 16 deletions.
51 changes: 51 additions & 0 deletions src/hooks/useEventListener.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { useEffect, useRef } from "react"

type ElementType = HTMLElement | Window | Document | null
type EventMap<Type extends ElementType> = Type extends HTMLElement
? HTMLElementEventMap
: Type extends Window
? WindowEventMap
: DocumentEventMap

interface UseEventListenerProps<
Type extends ElementType,
EventName extends keyof EventMap<Type>
> {
ref: Type
event: EventName
handler: (e: EventMap<Type>[EventName]) => void
disabled?: boolean
}

export const useEventListener = <
Type extends ElementType,
EventName extends keyof EventMap<Type>
>({
ref,
event,
handler,
disabled,
}: UseEventListenerProps<Type, EventName>) => {
const clear = useRef<() => void>(() => null)
const lastHandler = useRef(handler)
lastHandler.current = handler

useEffect(() => {
clear.current()
if (disabled || !ref) return

const emit = (e: EventMap<Type>[EventName]) => lastHandler.current(e)

ref.addEventListener(
event as string,
emit as EventListenerOrEventListenerObject
)
clear.current = () => {
ref.removeEventListener(
event as string,
emit as EventListenerOrEventListenerObject
)
}
return () => clear.current()
}, [disabled, event, ref])
}
95 changes: 95 additions & 0 deletions src/hooks/useTransition.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import { RefObject, useEffect, useMemo, useRef, useState } from "react"

import { useEventListener } from "./useEventListener"

interface TransitionStyles {
whileShow?: string
toShow?: string
toHide?: string
whileHide?: string
}

type TransitionState =
| "start-hide"
| "to-hide"
| "hide"
| "start-show"
| "to-show"
| "show"

const noStyles: Required<TransitionStyles> = {
whileShow: "",
toShow: "",
toHide: "",
whileHide: "",
}

const getStyles = (state: TransitionState, styles?: TransitionStyles) => {
const { whileShow, toShow, toHide, whileHide } = { ...noStyles, ...styles }
switch (state) {
case "start-hide":
return `${toShow}`
case "to-hide":
return `${toHide}`
case "hide":
return `${whileHide} ${toHide}`
case "start-show":
return `${toHide}`
case "to-show":
return `${toShow}`
case "show":
return `${whileShow} ${toShow}`
}
}

const getState = (transition: "idle" | "start" | "running", hide: boolean) => {
if (transition === "start") {
return hide ? "start-hide" : "start-show"
}
if (transition === "running") {
return hide ? "to-hide" : "to-show"
}
return hide ? "hide" : "show"
}

interface TransitionProps {
ref: RefObject<HTMLElement | null>
hide: boolean
styles?: TransitionStyles
}
export const useTransition = ({ ref, hide, styles }: TransitionProps) => {
const [transition, setTransition] = useState<"idle" | "start" | "running">(
"idle"
)
const lastHide = useRef(hide)

useEffect(() => {
if (lastHide.current === hide) return
lastHide.current = hide
setTransition("start")
}, [hide])

useEffect(() => {
if (transition !== "start") return
setTransition("running")
}, [transition])

useEventListener({
ref: ref.current,
event: "transitionend",
handler: () => {
console.log("transitionend")
setTransition("idle")
},
})

const state = useMemo(
() => getState(transition, lastHide.current),
[transition]
)

return {
state,
className: getStyles(state, styles),
}
}
62 changes: 46 additions & 16 deletions src/pages/overview/GamesTableBody.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { Dispatch } from "react"
import { Dispatch, useRef } from "react"

import { Cell, flexRender, Row, Table } from "@tanstack/react-table"
import { PenBox, Trash } from "lucide-react"

import { IconButton } from "~/components/IconButton"
import { Table as NativeTable } from "~/components/ui/table"
import { Game } from "~/data/games"
import { useTransition } from "~/hooks/useTransition"
import { cn } from "~/utils/utils"

interface TableActionsProps {
Expand Down Expand Up @@ -49,6 +50,49 @@ const DataCell = ({ cell }: { cell: Cell<Game, unknown> }) => (
</NativeTable.Cell>
)

interface GamesTableRowProps {
row: Row<Game>
onEdit: (game: Game) => void
onDelete: (game: Game) => void
}

const DataRow = ({ row, onDelete, onEdit }: GamesTableRowProps) => {
const ref = useRef<HTMLTableRowElement>(null)
const { state, className } = useTransition({
ref,
hide: row.isHidden(),
styles: {
toHide: "h-0 border-none opacity-0",
toShow: "h-12 border-b opacity-100",
whileHide: "hidden",
},
})

if (state === "hide") return null

return (
<NativeTable.Row
key={row.id}
ref={ref}
className={cn(
"overflow-hidden transition-[height,opacity] duration-300 motion-reduce:duration-0",
className
)}
>
{row.getVisibleCells().map(cell => (
<DataCell key={cell.id} cell={cell} />
))}

<TableActions
key={row.id}
row={row}
onEdit={onEdit}
onDelete={onDelete}
/>
</NativeTable.Row>
)
}

interface GamesTableBodyProps {
table: Table<Game>
onEdit: (game: Game) => void
Expand All @@ -63,21 +107,7 @@ export const GamesTableBody = ({
return (
<NativeTable.Body>
{table.getRowModel().rows.map(row => (
<NativeTable.Row
key={row.id}
className={cn(row.isHidden() && "hidden")}
>
{row.getVisibleCells().map(cell => (
<DataCell key={cell.id} cell={cell} />
))}

<TableActions
key={row.id}
row={row}
onEdit={onEdit}
onDelete={onDelete}
/>
</NativeTable.Row>
<DataRow key={row.id} row={row} onEdit={onEdit} onDelete={onDelete} />
))}
</NativeTable.Body>
)
Expand Down

0 comments on commit 8827d8a

Please sign in to comment.