summaryrefslogtreecommitdiff
path: root/components/Sidebar/Collapse.tsx
blob: b297605c5b2485d0d728ce09351aab4e8968ad87 (about) (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
// modified from https://github.com/chakra-ui/chakra-ui/blob/fc3b97d0978cf2adb9fc79157c6e42b4b68155c5/packages/transition/src/collapse.tsx

import { cx, mergeWith, warn, __DEV__ } from '@chakra-ui/utils'
import { AnimatePresence, HTMLMotionProps, motion, Variants as _Variants } from 'framer-motion'
import * as React from 'react'
import { TransitionEasings, Variants, withDelay, WithTransitionConfig } from './transition-utils'

const isNumeric = (value?: string | number) => value != null && parseInt(value.toString(), 10) > 0

export interface CollapseOptions {
  /**
   * If `true`, the opacity of the content will be animated
   * @default true
   */
  animateOpacity?: boolean
  /**
   * The size you want the content in its collapsed state.
   * @default 0
   */
  startingSize?: number | string
  /**
   * The size you want the content in its expanded state.
   * @default "auto"
   */
  endingSize?: number | string
  /**
   * The dimension you want to collapse by.
   * @default "size"
   */
  dimension?: string
}

const defaultTransitions = {
  exit: {
    size: { duration: 0.2, ease: TransitionEasings.ease },
    opacity: { duration: 0.3, ease: TransitionEasings.ease },
  },
  enter: {
    size: { duration: 0.3, ease: TransitionEasings.ease },
    opacity: { duration: 0.4, ease: TransitionEasings.ease },
  },
}

const variants: Variants<CollapseOptions> = {
  exit: ({ animateOpacity, startingSize, transition, transitionEnd, delay, dimension }) => ({
    ...(animateOpacity && { opacity: isNumeric(startingSize) ? 1 : 0 }),
    overflow: 'hidden',
    [dimension as string]: startingSize,
    transitionEnd: transitionEnd?.exit,
    transition: transition?.exit ?? withDelay.exit(defaultTransitions.exit, delay),
  }),
  enter: ({ animateOpacity, endingSize, transition, transitionEnd, delay, dimension }) => ({
    ...(animateOpacity && { opacity: 1 }),
    [dimension as string]: endingSize,
    transitionEnd: transitionEnd?.enter,
    transition: transition?.enter ?? withDelay.enter(defaultTransitions.enter, delay),
  }),
}

export type ICollapse = CollapseProps

export interface CollapseProps
  extends WithTransitionConfig<HTMLMotionProps<'div'>>,
    CollapseOptions {}

export const Collapse = React.forwardRef<HTMLDivElement, CollapseProps>((props, ref) => {
  const {
    in: isOpen,
    unmountOnExit,
    animateOpacity = true,
    startingSize = 0,
    endingSize = 'auto',
    dimension = 'height',
    style,
    className,
    transition,
    transitionEnd,
    ...rest
  } = props

  const [mounted, setMounted] = React.useState(false)
  React.useEffect(() => {
    const timeout = setTimeout(() => {
      setMounted(true)
    })
    return () => clearTimeout(timeout)
  }, [])

  /**
   * Warn 🚨: `startingSize` and `unmountOnExit` are mutually exclusive
   *
   * If you specify a starting size, the collapsed needs to be mounted
   * for the size to take effect.
   */
  warn({
    condition: Boolean(startingSize > 0 && unmountOnExit),
    message: `startingSize and unmountOnExit are mutually exclusive. You can't use them together`,
  })

  const hasStartingSize = parseFloat(startingSize.toString()) > 0

  const custom = {
    startingSize,
    endingSize,
    animateOpacity,
    dimension,
    transition: !mounted ? { enter: { duration: 0 } } : transition,
    transitionEnd: mergeWith(transitionEnd, {
      enter: { overflow: 'initial' },
      exit: unmountOnExit
        ? undefined
        : {
            display: hasStartingSize ? 'block' : 'none',
          },
    }),
  }

  const show = unmountOnExit ? isOpen : true
  const animate = isOpen || unmountOnExit ? 'enter' : 'exit'

  return (
    <AnimatePresence initial={false} custom={custom}>
      {show && (
        <motion.div
          ref={ref}
          {...rest}
          className={cx('chakra-collapse', className)}
          style={{
            overflow: 'hidden',
            display: 'block',
            ...style,
          }}
          custom={custom}
          variants={variants as _Variants}
          initial={unmountOnExit ? 'exit' : false}
          animate={animate}
          exit="exit"
        />
      )}
    </AnimatePresence>
  )
})

if (__DEV__) {
  Collapse.displayName = 'Collapse'
}