|
| 1 | +import { css, cx, keyframes } from '@leafygreen-ui/emotion'; |
| 2 | +import { createUniqueClassName, type Theme } from '@leafygreen-ui/lib'; |
| 3 | +import { |
| 4 | + addOverflowShadow, |
| 5 | + color, |
| 6 | + Side, |
| 7 | + spacing, |
| 8 | + transitionDuration, |
| 9 | +} from '@leafygreen-ui/tokens'; |
| 10 | + |
| 11 | +import { PANEL_WIDTH } from '../constants'; |
| 12 | + |
| 13 | +import { HEADER_HEIGHT, MOBILE_BREAKPOINT } from './drawer.constants'; |
| 14 | +import { DisplayMode } from './drawer.types'; |
| 15 | + |
| 16 | +export const drawerTransitionDuration = transitionDuration.slower; |
| 17 | + |
| 18 | +export const drawerClassName = createUniqueClassName('lg-drawer'); |
| 19 | + |
| 20 | +// Because of .show() and .close() in the drawer component, transitioning from 0px to (x)px does not transition correctly. Having the drawer start at the open position while hidden, moving to the closed position, and then animating to the open position is a workaround to get the animation to work. |
| 21 | +// These styles are used for a standalone drawer in overlay mode since it is not part of a grid layout. |
| 22 | +const drawerIn = keyframes` |
| 23 | + 0% { |
| 24 | + transform: translate3d(0%, 0, 0); |
| 25 | + opacity: 0; |
| 26 | + visibility: hidden; |
| 27 | + } |
| 28 | + 1% { |
| 29 | + transform: translate3d(100%, 0, 0); |
| 30 | + opacity: 1; |
| 31 | + visibility: visible; |
| 32 | + } |
| 33 | + 100% { |
| 34 | + transform: translate3d(0%, 0, 0); |
| 35 | + } |
| 36 | +`; |
| 37 | + |
| 38 | +// Keep the drawer opacity at 1 until the end of the animation. The inner container opacity is transitioned separately. |
| 39 | +const drawerOut = keyframes` |
| 40 | + 0% { |
| 41 | + transform: translate3d(0%, 0, 0); |
| 42 | + } |
| 43 | + 99% { |
| 44 | + transform: translate3d(100%, 0, 0); |
| 45 | + opacity: 1; |
| 46 | + } |
| 47 | + 100% { |
| 48 | + opacity: 0; |
| 49 | + visibility: hidden; |
| 50 | + } |
| 51 | +`; |
| 52 | + |
| 53 | +const getBaseStyles = ({ theme }: { theme: Theme }) => css` |
| 54 | + all: unset; |
| 55 | + background-color: ${color[theme].background.primary.default}; |
| 56 | + border: 1px solid ${color[theme].border.secondary.default}; |
| 57 | + width: 100%; |
| 58 | + max-width: ${PANEL_WIDTH}px; |
| 59 | + height: 100%; |
| 60 | + overflow: hidden; |
| 61 | + box-sizing: border-box; |
| 62 | +
|
| 63 | + @media only screen and (max-width: ${MOBILE_BREAKPOINT}px) { |
| 64 | + max-width: 100%; |
| 65 | + height: 50vh; |
| 66 | + } |
| 67 | +`; |
| 68 | + |
| 69 | +const overlayOpenStyles = css` |
| 70 | + opacity: 1; |
| 71 | + animation-name: ${drawerIn}; |
| 72 | +
|
| 73 | + // On mobile, the drawer should be positioned at the bottom of the screen when closed, and slide up to the top when opened. |
| 74 | + @media only screen and (max-width: ${MOBILE_BREAKPOINT}px) { |
| 75 | + transform: none; |
| 76 | + } |
| 77 | +`; |
| 78 | + |
| 79 | +const overlayClosedStyles = css` |
| 80 | + pointer-events: none; |
| 81 | + animation-name: ${drawerOut}; |
| 82 | +
|
| 83 | + // On mobile, the drawer should be positioned at the bottom of the screen when closed, and slide up to the top when opened. |
| 84 | + @media only screen and (max-width: ${MOBILE_BREAKPOINT}px) { |
| 85 | + transform: translate3d(0, 100%, 0); |
| 86 | + opacity: 0; |
| 87 | + } |
| 88 | +`; |
| 89 | + |
| 90 | +const getOverlayStyles = ({ |
| 91 | + open, |
| 92 | + shouldAnimate, |
| 93 | + zIndex, |
| 94 | +}: { |
| 95 | + open: boolean; |
| 96 | + shouldAnimate: boolean; |
| 97 | + zIndex: number; |
| 98 | +}) => |
| 99 | + cx( |
| 100 | + css` |
| 101 | + position: absolute; |
| 102 | + z-index: ${zIndex}; |
| 103 | + top: 0; |
| 104 | + bottom: 0; |
| 105 | + right: 0; |
| 106 | + overflow: visible; |
| 107 | +
|
| 108 | + // By default, the drawer is positioned off-screen to the right. |
| 109 | + transform: translate3d(100%, 0, 0); |
| 110 | + animation-timing-function: ease-in-out; |
| 111 | + animation-duration: ${drawerTransitionDuration}ms; |
| 112 | + animation-fill-mode: forwards; |
| 113 | +
|
| 114 | + @media only screen and (max-width: ${MOBILE_BREAKPOINT}px) { |
| 115 | + top: unset; |
| 116 | + left: 0; |
| 117 | + // Since the drawer has position: fixed, we can use normal transitions |
| 118 | + animation: none; |
| 119 | + position: fixed; |
| 120 | + transform: translate3d(0, 100%, 0); |
| 121 | + transition: transform ${drawerTransitionDuration}ms ease-in-out, |
| 122 | + opacity ${drawerTransitionDuration}ms ease-in-out |
| 123 | + ${open ? '0ms' : `${drawerTransitionDuration}ms`}; |
| 124 | + } |
| 125 | + `, |
| 126 | + { |
| 127 | + [overlayOpenStyles]: open, |
| 128 | + [overlayClosedStyles]: !open && shouldAnimate, // This ensures that the drawer does not animate closed on initial render |
| 129 | + } |
| 130 | + ); |
| 131 | + |
| 132 | +const getDisplayModeStyles = ({ |
| 133 | + displayMode, |
| 134 | + open, |
| 135 | + shouldAnimate, |
| 136 | + zIndex, |
| 137 | +}: { |
| 138 | + displayMode: DisplayMode; |
| 139 | + open: boolean; |
| 140 | + shouldAnimate: boolean; |
| 141 | + zIndex: number; |
| 142 | +}) => |
| 143 | + cx({ |
| 144 | + [getOverlayStyles({ open, shouldAnimate, zIndex })]: |
| 145 | + displayMode === DisplayMode.Overlay, |
| 146 | + }); |
| 147 | + |
| 148 | +export const getDrawerStyles = ({ |
| 149 | + className, |
| 150 | + displayMode, |
| 151 | + open, |
| 152 | + shouldAnimate, |
| 153 | + theme, |
| 154 | + zIndex, |
| 155 | +}: { |
| 156 | + className?: string; |
| 157 | + displayMode: DisplayMode; |
| 158 | + open: boolean; |
| 159 | + shouldAnimate: boolean; |
| 160 | + theme: Theme; |
| 161 | + zIndex: number; |
| 162 | +}) => |
| 163 | + cx( |
| 164 | + getBaseStyles({ theme }), |
| 165 | + getDisplayModeStyles({ displayMode, open, shouldAnimate, zIndex }), |
| 166 | + className, |
| 167 | + drawerClassName |
| 168 | + ); |
| 169 | + |
| 170 | +export const getDrawerShadowStyles = ({ |
| 171 | + theme, |
| 172 | + displayMode, |
| 173 | +}: { |
| 174 | + theme: Theme; |
| 175 | + displayMode: DisplayMode; |
| 176 | +}) => |
| 177 | + cx( |
| 178 | + css` |
| 179 | + height: 100%; |
| 180 | + background-color: ${color[theme].background.primary.default}; |
| 181 | + `, |
| 182 | + { |
| 183 | + [css` |
| 184 | + ${addOverflowShadow({ isInside: false, side: Side.Left, theme })}; |
| 185 | +
|
| 186 | + @media only screen and (max-width: ${MOBILE_BREAKPOINT}px) { |
| 187 | + ${addOverflowShadow({ isInside: false, side: Side.Top, theme })}; |
| 188 | + } |
| 189 | + `]: displayMode === DisplayMode.Overlay, |
| 190 | + } |
| 191 | + ); |
| 192 | + |
| 193 | +const getBaseInnerContainerStyles = ({ theme }: { theme: Theme }) => css` |
| 194 | + width: 100%; |
| 195 | + height: 100%; |
| 196 | + display: flex; |
| 197 | + flex-direction: column; |
| 198 | + background-color: ${color[theme].background.primary.default}; |
| 199 | + opacity: 0; |
| 200 | + transition-property: opacity; |
| 201 | + transition-duration: ${transitionDuration.faster}ms; |
| 202 | + transition-timing-function: linear; |
| 203 | +`; |
| 204 | + |
| 205 | +const getInnerOpenContainerStyles = css` |
| 206 | + transition-property: opacity; |
| 207 | + transition-duration: ${transitionDuration.slowest}ms; |
| 208 | + transition-timing-function: linear; |
| 209 | + opacity: 1; |
| 210 | +`; |
| 211 | + |
| 212 | +export const getInnerContainerStyles = ({ |
| 213 | + theme, |
| 214 | + open, |
| 215 | +}: { |
| 216 | + theme: Theme; |
| 217 | + open: boolean; |
| 218 | +}) => |
| 219 | + cx(getBaseInnerContainerStyles({ theme }), { |
| 220 | + [getInnerOpenContainerStyles]: open, |
| 221 | + }); |
| 222 | + |
| 223 | +export const getHeaderStyles = ({ theme }: { theme: Theme }) => css` |
| 224 | + height: ${HEADER_HEIGHT}px; |
| 225 | + padding: ${spacing[400]}px; |
| 226 | + display: flex; |
| 227 | + justify-content: space-between; |
| 228 | + align-items: center; |
| 229 | + border-bottom: 1px solid ${color[theme].border.secondary.default}; |
| 230 | + transition-property: box-shadow; |
| 231 | + transition-duration: ${transitionDuration.faster}ms; |
| 232 | + transition-timing-function: ease-in-out; |
| 233 | +`; |
| 234 | + |
| 235 | +const baseChildrenContainerStyles = css` |
| 236 | + height: 100%; |
| 237 | + overflow: hidden; |
| 238 | +`; |
| 239 | + |
| 240 | +export const getChildrenContainerStyles = ({ |
| 241 | + hasShadowTop, |
| 242 | + theme, |
| 243 | +}: { |
| 244 | + hasShadowTop: boolean; |
| 245 | + theme: Theme; |
| 246 | +}) => |
| 247 | + cx(baseChildrenContainerStyles, { |
| 248 | + [addOverflowShadow({ isInside: true, side: Side.Top, theme })]: |
| 249 | + hasShadowTop, |
| 250 | + }); |
| 251 | + |
| 252 | +const baseInnerChildrenContainerStyles = css` |
| 253 | + height: 100%; |
| 254 | +`; |
| 255 | + |
| 256 | +const scrollContainerStyles = css` |
| 257 | + padding: ${spacing[400]}px; |
| 258 | + overflow-y: auto; |
| 259 | + overscroll-behavior: contain; |
| 260 | +`; |
| 261 | + |
| 262 | +export const innerChildrenContainerStyles = cx( |
| 263 | + baseInnerChildrenContainerStyles, |
| 264 | + scrollContainerStyles |
| 265 | +); |
0 commit comments