UNPKG

@navinc/base-react-components

Version:
313 lines (283 loc) 8.42 kB
import { Component } from 'react' import styled from 'styled-components' import debounce from 'lodash.debounce' import propTypes from 'prop-types' import { Button } from '../../button.js' import { Copy } from '../../copy.js' import { Header } from '../../header.js' import { KebabMenu } from './parts/kebab-menu.js' const depricationWarning = (isInUse, warning) => { isInUse && console.warn(warning) } /* Marks the bottom of the card's content. */ const WayMark = styled.hr` border: none; margin: 0; height: 0; ` WayMark.displayName = 'WayMark' const CARD_BODY_PADDING_BOTTOM = 32 export const CardHeader = styled(Copy).attrs(() => ({ size: 'sm' }))` display: flex; flex-flow: row nowrap; justify-content: space-between; align-items: center; min-height: 24px; padding-bottom: 16px; color: ${({ theme }) => theme.neutral400}; ` CardHeader.displayName = 'CardHeader' export const Title = styled(Header).attrs(() => ({ size: 'md' }))` padding-bottom: 16px; ` Title.displayName = 'Title' export const CardBody = styled.div` position: relative; /* required to get offsetTop of Waymark from Card */ padding-bottom: ${CARD_BODY_PADDING_BOTTOM}px; ${({ height, shouldNotAnimate }) => shouldNotAnimate ? '' : 'transition: height 250ms ease-in-out;' + `height: ${height}px;`} overflow: hidden; ${Copy} { max-width: 520px; } ` CardBody.displayName = 'CardBody' export const StyledCard = styled.div` position: relative; flex: 1 1 100%; padding: 24px; border-radius: 4px; background-color: ${({ theme }) => theme.white}; overflow: hidden; box-shadow: 0 0 3px 0 ${({ theme }) => theme.neutral300}, 0 1px 2px 0 ${({ theme }) => theme.neutral300}; ${({ isCardHidden, theme }) => isCardHidden && ` & > ${CardHeader} { border-top: 4px solid ${theme.good}; } `}; ` StyledCard.displayName = 'StyledCard' export const CloseIcon = styled.img` padding-left: 8px; ` CloseIcon.displayName = 'CloseIcon' export const CheckMark = styled.img` padding-right: 8px; ` CheckMark.displayName = 'CheckMark' export const DismissCard = styled.div` position: absolute; right: 8px; display: inline-block; height: 100%; padding: 16px; border-left: 2px solid ${({ theme }) => theme.neutral300}; cursor: pointer; ` DismissCard.displayName = 'DismissCard' export const Undo = styled(Copy)` cursor: pointer; font-weight: bold; margin-right: 64px; ` Undo.displayName = 'Undo' export const BackButton = styled(Button)` padding: 8px; margin-right: auto; ` BackButton.displayName = 'BackButton' export const NextButton = styled(Button)` &[disabled] { background-color: unset; border-color: transparent; color: ${({ theme }) => theme.neutral400}; cursor: default; } ` NextButton.displayName = 'NextButton' export const Kebab = styled.img.attrs(() => ({ src: 'https://dxkdvuv3hanyu.cloudfront.net/design-assets/icons/kebab-menu.svg', }))` position: absolute; top: 16px; right: 8px; padding: 8px; cursor: pointer; ` Kebab.displayName = 'Kebab' export const Footer = styled.div` display: flex; align-items: center; justify-content: flex-end; padding-top: 16px; border-top: 1px solid ${({ theme }) => theme.neutral300}; ` Footer.displayName = 'Footer' class _Card extends Component { static propTypes = { buttonCopy: propTypes.string, children: propTypes.node, className: propTypes.string, hasFooter: propTypes.bool, labelCopy: propTypes.string, onBack: propTypes.func, onDismiss: propTypes.func, onNext: propTypes.func, onHide: propTypes.func, onToggleKebab: propTypes.func, onUndo: propTypes.func, titleCopy: propTypes.string, buttonDisabled: propTypes.bool, buttonForm: propTypes.string, controlledHeight: propTypes.bool, kebabMenuItems: propTypes.array, shouldNotAnimate: propTypes.bool, } state = { isKebabMenuVisible: false, isCardHidden: false, isCardDismissed: false, contentHeight: 0, } toggleKebabMenu = (event) => { event.stopPropagation() this._mounted && this.setState({ isKebabMenuVisible: !this.state.isKebabMenuVisible }) } togglehideCard = (event) => { event.stopPropagation() if (this.state.isKebabMenuVisible) { this.toggleKebabMenu(event) } this._mounted && this.setState(({ isCardHidden }) => ({ isCardHidden: !isCardHidden })) } dismissCard = (_event) => { this._mounted && this.setState(() => ({ isCardDismissed: true })) } setRef = (name) => (ref) => { this[name] = ref } setHeight = debounce( () => { if (this._mounted && !this.props.shouldNotAnimate) { this.setState(() => ({ contentHeight: (this.wayMark?.offsetTop ?? 0) + CARD_BODY_PADDING_BOTTOM, })) } }, 250, { leading: true } ) render() { const { buttonCopy, buttonLabel = buttonCopy, children, className, labelCopy, cardLabel = labelCopy, hasAsyncNextButton, isLoading, onBack, onNext, onHide = () => {}, onToggleKebab = () => {}, titleCopy, title = titleCopy, buttonDisabled, buttonForm, kebabMenuItems = [], hasFooter = !!(onNext || onBack || buttonForm), shouldNotAnimate, } = this.props const { isKebabMenuVisible, isCardHidden, isCardDismissed } = this.state if (isCardDismissed) return null return ( <StyledCard className={className} isCardHidden={isCardHidden}> {cardLabel && <CardHeader>{cardLabel}</CardHeader>} {!!kebabMenuItems.length && ( <Kebab data-testid="deprecated-card:kebab" onClick={(event) => { this.toggleKebabMenu(event) onToggleKebab(event) }} /> )} {isKebabMenuVisible && ( <KebabMenu kebabMenuItems={kebabMenuItems} onHide={onHide} togglehideCard={this.togglehideCard} toggleKebabMenu={this.toggleKebabMenu} /> )} {title && <Title>{title}</Title>} <CardBody height={this.state.contentHeight} shouldNotAnimate={shouldNotAnimate}> {children} <WayMark ref={this.setRef('wayMark')} /> </CardBody> {hasFooter && ( <Footer> {onBack && ( <BackButton onClick={onBack} variation="noOutline"> <img src="https://dxkdvuv3hanyu.cloudfront.net/icons/card_back_arrow.svg" alt="back arrow" /> </BackButton> )} {!hasAsyncNextButton && onNext && ( <NextButton isLoading={isLoading} onClick={onNext} size="cardButton" variation="noOutline" disabled={buttonDisabled} form={buttonForm} > {buttonLabel} </NextButton> )} {hasAsyncNextButton && onNext && ( <Button isLoading={isLoading} onClick={onNext}> {buttonLabel} </Button> )} </Footer> )} </StyledCard> ) } componentDidMount() { this._mounted = true this.setHeight() window.addEventListener('resize', this.setHeight) depricationWarning( this.props.buttonCopy, 'The `buttonCopy` property on the `Card` component has been depricated and renamed to `buttonLabel`' ) depricationWarning( this.props.titleCopy, 'The `titleCopy` property on the `Card` component has been depricated and renamed to `title`' ) depricationWarning( this.props.controlledHeight, 'The `controlledHeight` property on the `Card` component has been depricated. All card components have a transition animation when heights change. The consumer of the Card component does not need to set explicit heights.' ) depricationWarning( this.props.labelCopy, 'The `labelCopy` property on the `Card` component has been depricated and renamed `cardLabel`.' ) } componentDidUpdate() { if (this.state.contentHeight !== (this.wayMark?.offsetTop ?? 0)) { this.setHeight() } } componentWillUnmount() { this._mounted = false window.removeEventListener('resize', this.setHeight) } } export const Card = styled(_Card)``