1import * as React from 'react' 2import * as Styles from '../styles' 3import ClickableBox from './clickable-box' 4import {Box2} from './box' 5import BoxGrow from './box-grow' 6import Divider from './divider' 7import './list-item2.css' 8 9const Kb = { 10 Box2, 11 BoxGrow, 12 ClickableBox, 13 Divider, 14} 15 16// List item following stylesheet specs. TODO deprecate list-item.*.js 17 18type Props = { 19 type: 'Small' | 'Large' 20 icon?: React.ReactNode 21 statusIcon?: React.ReactNode 22 body: React.ReactNode 23 firstItem: boolean 24 fullDivider?: boolean 25 action?: React.ReactNode 26 hideHover?: boolean 27 // If 'grow' is used, the width of action cannot exceed 64. If larger width 28 // is needed, bump the `maxWidth: 64` below to a larger value. Note that if 29 // it's too large, the animation would also seem much faster. 30 onlyShowActionOnHover?: 'fade' | 'grow' | null 31 onClick?: () => void 32 onMouseDown?: (evt: React.BaseSyntheticEvent) => void // desktop only 33 height?: number // optional, for non-standard heights 34 style?: Styles.StylesCrossPlatform 35 iconStyleOverride?: Styles.StylesCrossPlatform 36 containerStyleOverride?: Styles.StylesCrossPlatform 37} 38 39const ListItem = (props: Props) => ( 40 <Kb.ClickableBox 41 onClick={props.onClick || (props.onMouseDown ? () => {} : undefined)} // make sure clickable box applies click styles if just onMouseDown is given. 42 onMouseDown={props.onMouseDown} 43 style={Styles.collapseStyles([ 44 props.type === 'Small' ? styles.clickableBoxSmall : styles.clickableBoxLarge, 45 !!props.height && {minHeight: props.height}, 46 props.style, 47 ])} 48 > 49 <Kb.Box2 50 className={Styles.classNames({ 51 listItem2: !props.hideHover, 52 })} 53 direction="horizontal" 54 style={Styles.collapseStyles([ 55 props.type === 'Small' ? styles.rowSmall : styles.rowLarge, 56 !!props.height && {minHeight: props.height}, 57 ])} 58 fullWidth={true} 59 > 60 {!props.firstItem && !!props.fullDivider && <Divider style={styles.divider} />} 61 {props.statusIcon && ( 62 <Kb.Box2 63 direction="vertical" 64 style={getStatusIconStyle(props)} 65 alignSelf="flex-start" 66 alignItems="center" 67 > 68 {props.statusIcon} 69 </Kb.Box2> 70 )} 71 {props.icon && ( 72 <Kb.Box2 73 direction="vertical" 74 style={getIconStyle(props)} 75 centerChildren={true} 76 alignSelf="flex-start" 77 > 78 {props.icon} 79 </Kb.Box2> 80 )} 81 <Kb.Box2 direction="horizontal" style={getContainerStyles(props)}> 82 {!props.firstItem && !props.fullDivider && <Divider style={styles.divider} />} 83 <Kb.BoxGrow> 84 <Kb.Box2 fullHeight={true} direction="horizontal" style={styles.bodyContainer}> 85 {props.body} 86 </Kb.Box2> 87 </Kb.BoxGrow> 88 <Kb.Box2 89 direction="horizontal" 90 className={Styles.classNames({ 91 fade: props.onlyShowActionOnHover === 'fade', 92 grow: props.onlyShowActionOnHover === 'grow', 93 })} 94 style={getActionStyle(props)} 95 alignSelf="flex-start" 96 > 97 {props.action} 98 </Kb.Box2> 99 </Kb.Box2> 100 </Kb.Box2> 101 </Kb.ClickableBox> 102) 103 104export const smallHeight = Styles.isMobile ? 56 : 48 105export const largeHeight = Styles.isMobile ? 64 : 56 106const smallIconWidth = Styles.isMobile ? 64 : 64 107const largeIconWidth = Styles.isMobile ? 72 : 72 108const statusIconWidth = Styles.isMobile ? 48 : 44 109const afterStatusIconItemLeftDistance = statusIconWidth - (Styles.isMobile ? 10 : 14) 110 111const styles = Styles.styleSheetCreate(() => { 112 const _styles = { 113 actionLargeContainer: { 114 alignItems: 'center', 115 flexGrow: 0, 116 flexShrink: 0, 117 justifyContent: 'flex-start', 118 marginRight: 8, 119 position: 'relative', 120 } as const, 121 actionSmallContainer: { 122 alignItems: 'center', 123 flexGrow: 0, 124 flexShrink: 0, 125 justifyContent: 'flex-start', 126 marginRight: 8, 127 position: 'relative', 128 } as const, 129 bodyContainer: { 130 alignItems: 'center', 131 flexGrow: 1, 132 flexShrink: 1, 133 justifyContent: 'flex-start', 134 maxWidth: '100%', 135 position: 'relative', 136 } as const, 137 clickableBoxLarge: { 138 flexShrink: 0, 139 minHeight: largeHeight, 140 width: '100%', 141 } as const, 142 clickableBoxSmall: { 143 flexShrink: 0, 144 minHeight: smallHeight, 145 width: '100%', 146 } as const, 147 contentContainer: { 148 flexGrow: 1, 149 position: 'relative', 150 } as const, 151 divider: { 152 left: 0, 153 position: 'absolute', 154 right: 0, 155 top: 0, 156 } as const, 157 heightLarge: {minHeight: largeHeight} as const, 158 heightSmall: {minHeight: smallHeight} as const, 159 icon: { 160 position: 'absolute', 161 } as const, 162 iconLarge: { 163 minHeight: largeHeight, 164 width: largeIconWidth, 165 } as const, 166 iconSmall: { 167 minHeight: smallHeight, 168 width: smallIconWidth, 169 } as const, 170 rowLarge: { 171 alignItems: 'center', 172 minHeight: largeHeight, 173 position: 'relative', 174 } as const, 175 rowSmall: { 176 alignItems: 'center', 177 minHeight: smallHeight, 178 position: 'relative', 179 } as const, 180 statusIcon: { 181 justifyContent: 'center', 182 position: 'absolute', 183 width: statusIconWidth, 184 } as const, 185 } 186 187 const _statusIconStyles = { 188 statusIconLarge: { 189 ..._styles.statusIcon, 190 ..._styles.heightLarge, 191 }, 192 statusIconSmall: { 193 ..._styles.statusIcon, 194 ..._styles.heightSmall, 195 }, 196 } 197 198 const _iconStyles = { 199 iconLargeWithNoStatusIcon: { 200 ..._styles.icon, 201 ..._styles.iconLarge, 202 }, 203 iconLargeWithStatusIcon: { 204 ..._styles.icon, 205 ..._styles.iconLarge, 206 left: afterStatusIconItemLeftDistance, 207 }, 208 iconSmallWithNoStatusIcon: { 209 ..._styles.icon, 210 ..._styles.iconSmall, 211 }, 212 iconSmallWithStatusIcon: { 213 ..._styles.icon, 214 ..._styles.iconSmall, 215 left: afterStatusIconItemLeftDistance, 216 }, 217 } 218 219 // Using margin and keeping the icon on the let using absolute is to work around an issue in RN where 220 // the nested views won't grow and still retain their widths correctly 221 const _containerStyles = { 222 containerLargeNoStatusIconNoIcon: { 223 ..._styles.contentContainer, 224 ..._styles.heightLarge, 225 marginLeft: Styles.globalMargins.tiny, 226 }, 227 containerLargeNoStatusIconWithIcon: { 228 ..._styles.contentContainer, 229 ..._styles.heightLarge, 230 marginLeft: largeIconWidth, 231 }, 232 containerLargeWithStatusIconNoIcon: { 233 ..._styles.contentContainer, 234 ..._styles.heightLarge, 235 marginLeft: afterStatusIconItemLeftDistance, 236 }, 237 containerLargeWithStatusIconWithIcon: { 238 ..._styles.contentContainer, 239 ..._styles.heightLarge, 240 marginLeft: largeIconWidth + afterStatusIconItemLeftDistance, 241 }, 242 containerSmallNoStatusIconNoIcon: { 243 ..._styles.contentContainer, 244 ..._styles.heightSmall, 245 marginLeft: Styles.globalMargins.tiny, 246 }, 247 containerSmallNoStatusIconWithIcon: { 248 ..._styles.contentContainer, 249 ..._styles.heightSmall, 250 marginLeft: smallIconWidth, 251 }, 252 containerSmallWithStatusIconNoIcon: { 253 ..._styles.contentContainer, 254 ..._styles.heightSmall, 255 marginLeft: afterStatusIconItemLeftDistance, 256 }, 257 containerSmallWithStatusIconWithIcon: { 258 ..._styles.contentContainer, 259 ..._styles.heightSmall, 260 marginLeft: smallIconWidth + afterStatusIconItemLeftDistance, 261 }, 262 } 263 264 const _actionStyles = { 265 actionLargeIsGrowOnHover: { 266 ..._styles.actionLargeContainer, 267 ..._styles.heightLarge, 268 justifyContent: 'flex-end', 269 } as const, 270 actionLargeNotGrowOnHover: { 271 ..._styles.actionLargeContainer, 272 ..._styles.heightLarge, 273 } as const, 274 actionSmallIsGrowOnHover: { 275 ..._styles.actionSmallContainer, 276 ..._styles.heightSmall, 277 justifyContent: 'flex-end', 278 } as const, 279 actionSmallNotGrowOnHover: { 280 ..._styles.actionSmallContainer, 281 ..._styles.heightSmall, 282 } as const, 283 } 284 return { 285 ..._styles, 286 ..._statusIconStyles, 287 ..._iconStyles, 288 ..._containerStyles, 289 ..._actionStyles, 290 } 291}) 292 293const getStatusIconStyle = (props: Props) => 294 props.type === 'Small' ? styles.statusIconSmall : styles.statusIconLarge 295 296const getIconStyle = (props: Props) => 297 props.iconStyleOverride ?? 298 (props.type === 'Small' 299 ? props.statusIcon 300 ? styles.iconSmallWithStatusIcon 301 : styles.iconSmallWithNoStatusIcon 302 : props.statusIcon 303 ? styles.iconLargeWithStatusIcon 304 : styles.iconLargeWithNoStatusIcon) 305 306const getContainerStyles = (props: Props) => 307 Styles.collapseStyles([ 308 props.type === 'Small' 309 ? props.statusIcon 310 ? props.icon 311 ? styles.containerSmallWithStatusIconWithIcon 312 : styles.containerSmallWithStatusIconNoIcon 313 : props.icon 314 ? styles.containerSmallNoStatusIconWithIcon 315 : styles.containerSmallNoStatusIconNoIcon 316 : props.statusIcon 317 ? props.icon 318 ? styles.containerLargeWithStatusIconWithIcon 319 : styles.containerLargeWithStatusIconNoIcon 320 : props.icon 321 ? styles.containerLargeNoStatusIconWithIcon 322 : styles.containerLargeNoStatusIconNoIcon, 323 // If this becomes a problem, memoize different heights we use. 324 !!props.height && {minHeight: props.height}, 325 props.containerStyleOverride, 326 ]) 327 328const getActionStyle = (props: Props) => 329 Styles.collapseStyles([ 330 props.type === 'Small' 331 ? props.onlyShowActionOnHover 332 ? styles.actionSmallIsGrowOnHover 333 : styles.actionSmallNotGrowOnHover 334 : props.onlyShowActionOnHover 335 ? styles.actionLargeIsGrowOnHover 336 : styles.actionLargeNotGrowOnHover, 337 !!props.height && {minHeight: props.height}, 338 ]) 339 340export default ListItem 341