Skip to content

Commit

Permalink
feat(card): streamline Card Feedback API and DOM structure
Browse files Browse the repository at this point in the history
Make Card Feedback API match Button, and make the InteractionFeedback be a child of Card instead of
a conditional wrapper
  • Loading branch information
aVileBroker committed Sep 1, 2020
1 parent d129345 commit dc7535b
Show file tree
Hide file tree
Showing 4 changed files with 269 additions and 113 deletions.
5 changes: 2 additions & 3 deletions packages/hs-react-ui/src/components/Card/Card.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,7 @@ storiesOf('Card', module)
header={text('header', 'Card title')}
footer={text('footer', 'Actionable buttons, whatever other stuff you want to pass in!')}
elevation={number('elevation', 2, { range: true, min: -5, max: 5, step: 1 })}
onClick={action('onClick')}
disableFeedback={boolean('disableFeedback', false)}
onClick={boolean('onClick', true) ? action('onClick') : undefined}
feedbackType={select('feedbackType', feedbackTypes, feedbackTypes.ripple)}
>
{text(
Expand Down Expand Up @@ -90,7 +89,7 @@ storiesOf('Card', module)
footer={text('footer', 'Actionable buttons, whatever other stuff you want to pass in!')}
elevation={number('elevation', 0, { range: true, min: -5, max: 5, step: 1 })}
onClick={action('onClick')}
disableFeedback={boolean('disableFeedback', true)}
feedbackType={select('feedbackType', feedbackTypes, feedbackTypes.ripple)}
>
{text(
'children',
Expand Down
111 changes: 43 additions & 68 deletions packages/hs-react-ui/src/components/Card/Card.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
import React, { ReactNode, MouseEvent } from 'react';
import styled, { StyledComponentBase } from 'styled-components';
import useMeasure from 'react-use-measure';
import { ResizeObserver } from '@juggle/resize-observer';
import { darken } from 'polished';

import timings from '../../enums/timings';
Expand All @@ -14,36 +12,43 @@ import InteractionFeedback, {
} from '../InteractionFeedback/InteractionFeedback';
import FeedbackTypes from '../../enums/feedbackTypes';

const defaultOnClick = () => {};

export type CardContainerProps = {
elevation: number;
feedbackType: FeedbackTypes;
disableFeedback: boolean;
onClick: (evt: MouseEvent) => void;
};

export const CardContainer = styled(Div)`
${({ elevation, feedbackType, disableFeedback }: CardContainerProps) => {
${({ elevation, feedbackType, onClick }: CardContainerProps) => {
const { colors } = useTheme();
return `
${onClick !== defaultOnClick ? 'cursor: pointer;' : ''}
display: inline-flex;
flex-flow: column nowrap;
font-size: 1rem;
border-radius: 0.25rem;
border: ${!elevation ? `1px solid ${colors.grayXlight}` : '0px solid transparent'};
transition: filter ${timings.slow}, box-shadow ${timings.slow}, border ${timings.normal};
transition:
filter ${timings.slow},
box-shadow ${timings.slow},
border ${timings.normal},
background-color ${timings.normal};
${getShadowStyle(elevation, colors.shadow)}
background-color: ${colors.background};
${
feedbackType === FeedbackTypes.simple && !disableFeedback
feedbackType === FeedbackTypes.simple && onClick !== defaultOnClick
? `
&:active {
background-color: ${
colors.background !== 'transparent'
? darken(0.1, colors.background)
: 'rgba(0, 0, 0, 0.1)'
};
}
`
&:active {
background-color: ${
colors.background !== 'transparent'
? darken(0.1, colors.background)
: 'rgba(0, 0, 0, 0.1)'
};
}
`
: ''
}
`;
Expand Down Expand Up @@ -97,6 +102,14 @@ export const Footer = styled(Div)`
}}
`;

const StyledFeedbackContainer = styled(InteractionFeedback.Container)`
position: absolute;
width: 100%;
height: 100%;
left: 0;
top: 0;
`;

export interface CardProps {
StyledContainer?: string & StyledComponentBase<any, {}>;
StyledHeader?: string & StyledComponentBase<any, {}>;
Expand All @@ -120,8 +133,6 @@ export interface CardProps {
feedbackType?: FeedbackTypes;
}

const defaultOnClick = () => {};

const Card = ({
StyledContainer = CardContainer,
StyledHeader = Header,
Expand All @@ -141,70 +152,34 @@ const Card = ({
footer,

elevation = 1,
disableFeedback,
feedbackType = FeedbackTypes.ripple,
}: CardProps): JSX.Element | null => {
const { colors } = useTheme();
// get the bounding box of the card so that we can set it's width to r
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const [ref, cardBounds] = useMeasure({ polyfill: ResizeObserver });

if (!disableFeedback && feedbackType !== FeedbackTypes.simple && onClick !== defaultOnClick) {
// 5% larger than the width to account for the circle to cover the entire card
const feedbackRadius = cardBounds.width * 1.05;
const feedbackRatio = feedbackRadius / 100; // 100 is the default r
const tension = (750 / feedbackRatio) * 2; // 750 is the default tension, x2 makes it look a little quicker over large cards
const transitionProps = {
from: {
r: 0,
opacity: 0.25,
fill: colors.grayLight,
},
enter: {
r: feedbackRadius,
opacity: 0.25,
fill: colors.grayLight,
},
leave: {
r: 0,
opacity: 0,
fill: colors.grayLight,
},
config: {
mass: 1,
tension,
friction: 35,
},
};
return (
<InteractionFeedback transitionProps={transitionProps} {...interactionFeedbackProps}>
<StyledContainer
ref={ref}
onClick={onClick}
elevation={elevation}
feedbackType={feedbackType}
disableFeedback={disableFeedback || onClick === defaultOnClick}
{...containerProps}
>
{header && <StyledHeader {...headerProps}>{header}</StyledHeader>}
{children && <StyledBody {...bodyProps}>{children}</StyledBody>}
{footer && <StyledFooter {...footerProps}>{footer}</StyledFooter>}
</StyledContainer>
</InteractionFeedback>
);
}
const transitionProps = {
...InteractionFeedback.defaultTransitionProps,
enter: {
...InteractionFeedback.defaultTransitionProps,
r: 300,
},
};

return (
<StyledContainer
onClick={onClick}
elevation={elevation}
feedbackType={feedbackType}
disableFeedback={disableFeedback || onClick === defaultOnClick}
{...containerProps}
>
{header && <StyledHeader {...headerProps}>{header}</StyledHeader>}
{children && <StyledBody {...bodyProps}>{children}</StyledBody>}
{footer && <StyledFooter {...footerProps}>{footer}</StyledFooter>}
{feedbackType !== FeedbackTypes.simple && onClick !== defaultOnClick && (
<InteractionFeedback
color="rgba(0,0,0,0.1)"
transitionProps={transitionProps}
StyledContainer={StyledFeedbackContainer}
{...interactionFeedbackProps}
/>
)}
</StyledContainer>
);
};
Expand Down
35 changes: 29 additions & 6 deletions packages/hs-react-ui/src/components/Card/__tests__/Card.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,24 +37,47 @@ describe('Card', () => {
});

it('shows Card with default feedback', async () => {
const { container, getByTestId } = render(
<Card onClick={() => {}} containerProps={{ 'data-test-id': testId }} />,
);
await waitFor(() => getByTestId(testId));
expect(container).toMatchSnapshot();
});

it('shows Card with simple feedback and no onClick', async () => {
const { container, getByTestId } = render(
<Card containerProps={{ 'data-test-id': testId }} feedbackType={FeedbackTypes.simple} />,
);
await waitFor(() => getByTestId(testId));
expect(container).toMatchSnapshot();
});

it('shows Card with ripple feedback and no onClick', async () => {
const { container, getByTestId } = render(
<Card containerProps={{ 'data-test-id': testId }} feedbackType={FeedbackTypes.ripple} />,
);
await waitFor(() => getByTestId(testId));
expect(container).toMatchSnapshot();
});

it('shows Card with simple feedback with onClick', async () => {
const { container, getByTestId } = render(
<Card
onClick={() => {}}
onClick={() => null}
containerProps={{ 'data-test-id': testId }}
disableFeedback={true}
feedbackType={FeedbackTypes.simple}
/>,
);
await waitFor(() => getByTestId(testId));
expect(container).toMatchSnapshot();
});

it('shows Card with non-default feedback', async () => {
it('shows Card with ripple feedback with onClick', async () => {
const { container, getByTestId } = render(
<Card
onClick={() => {}}
onClick={() => null}
containerProps={{ 'data-test-id': testId }}
disableFeedback={true}
feedbackType={FeedbackTypes.simple}
feedbackType={FeedbackTypes.ripple}
/>,
);
await waitFor(() => getByTestId(testId));
Expand Down
Loading

0 comments on commit dc7535b

Please sign in to comment.