@navinc/base-react-components
Version:
Nav's Pattern Library
313 lines (283 loc) • 8.42 kB
JavaScript
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)``