From dc7535bd1306f92e284e00d848677c07ceabdd89 Mon Sep 17 00:00:00 2001 From: aVileBroker Date: Tue, 1 Sep 2020 11:38:34 -0500 Subject: [PATCH] feat(card): streamline Card Feedback API and DOM structure Make Card Feedback API match Button, and make the InteractionFeedback be a child of Card instead of a conditional wrapper --- .../src/components/Card/Card.stories.tsx | 5 +- .../hs-react-ui/src/components/Card/Card.tsx | 111 ++++----- .../components/Card/__tests__/Card.test.tsx | 35 ++- .../__snapshots__/Card.test.tsx.snap | 231 +++++++++++++++--- 4 files changed, 269 insertions(+), 113 deletions(-) diff --git a/packages/hs-react-ui/src/components/Card/Card.stories.tsx b/packages/hs-react-ui/src/components/Card/Card.stories.tsx index 0daa4b448..34142a207 100644 --- a/packages/hs-react-ui/src/components/Card/Card.stories.tsx +++ b/packages/hs-react-ui/src/components/Card/Card.stories.tsx @@ -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( @@ -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', diff --git a/packages/hs-react-ui/src/components/Card/Card.tsx b/packages/hs-react-ui/src/components/Card/Card.tsx index 17cf64e90..a7bfb3521 100644 --- a/packages/hs-react-ui/src/components/Card/Card.tsx +++ b/packages/hs-react-ui/src/components/Card/Card.tsx @@ -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'; @@ -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)' + }; + } + ` : '' } `; @@ -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; StyledHeader?: string & StyledComponentBase; @@ -120,8 +133,6 @@ export interface CardProps { feedbackType?: FeedbackTypes; } -const defaultOnClick = () => {}; - const Card = ({ StyledContainer = CardContainer, StyledHeader = Header, @@ -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 ( - - - {header && {header}} - {children && {children}} - {footer && {footer}} - - - ); - } + const transitionProps = { + ...InteractionFeedback.defaultTransitionProps, + enter: { + ...InteractionFeedback.defaultTransitionProps, + r: 300, + }, + }; + return ( {header && {header}} {children && {children}} {footer && {footer}} + {feedbackType !== FeedbackTypes.simple && onClick !== defaultOnClick && ( + + )} ); }; diff --git a/packages/hs-react-ui/src/components/Card/__tests__/Card.test.tsx b/packages/hs-react-ui/src/components/Card/__tests__/Card.test.tsx index 6b2242a3a..d463150da 100644 --- a/packages/hs-react-ui/src/components/Card/__tests__/Card.test.tsx +++ b/packages/hs-react-ui/src/components/Card/__tests__/Card.test.tsx @@ -37,24 +37,47 @@ describe('Card', () => { }); it('shows Card with default feedback', async () => { + const { container, getByTestId } = render( + {}} 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( + , + ); + await waitFor(() => getByTestId(testId)); + expect(container).toMatchSnapshot(); + }); + + it('shows Card with ripple feedback and no onClick', async () => { + const { container, getByTestId } = render( + , + ); + await waitFor(() => getByTestId(testId)); + expect(container).toMatchSnapshot(); + }); + + it('shows Card with simple feedback with onClick', async () => { const { container, getByTestId } = render( {}} + 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( {}} + onClick={() => null} containerProps={{ 'data-test-id': testId }} - disableFeedback={true} - feedbackType={FeedbackTypes.simple} + feedbackType={FeedbackTypes.ripple} />, ); await waitFor(() => getByTestId(testId)); diff --git a/packages/hs-react-ui/src/components/Card/__tests__/__snapshots__/Card.test.tsx.snap b/packages/hs-react-ui/src/components/Card/__tests__/__snapshots__/Card.test.tsx.snap index 1e3a846f0..8dfe23d06 100644 --- a/packages/hs-react-ui/src/components/Card/__tests__/__snapshots__/Card.test.tsx.snap +++ b/packages/hs-react-ui/src/components/Card/__tests__/__snapshots__/Card.test.tsx.snap @@ -1,7 +1,15 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Card shows Card with default feedback 1`] = ` +.c2 { + pointer-events: none; + position: absolute; + top: 0; + left: 0; +} + .c0 { + cursor: pointer; display: -webkit-inline-box; display: -webkit-inline-flex; display: -ms-inline-flexbox; @@ -12,27 +20,43 @@ exports[`Card shows Card with default feedback 1`] = ` font-size: 1rem; border-radius: 0.25rem; border: 0px solid transparent; - -webkit-transition: filter .5s,box-shadow .5s,border .3s; - transition: filter .5s,box-shadow .5s,border .3s; + -webkit-transition: filter .5s, box-shadow .5s, border .3s, background-color .3s; + transition: filter .5s, box-shadow .5s, border .3s, background-color .3s; -webkit-filter: drop-shadow(0rem 0.06653090368236622rem 0.25rem rgba(39,47,78,0.4292196217912265)); filter: drop-shadow(0rem 0.06653090368236622rem 0.25rem rgba(39,47,78,0.4292196217912265)); background-color: #fff; } +.c1 { + position: relative; + position: absolute; + width: 100%; + height: 100%; + left: 0; + top: 0; +} +
+ > +
+ +
+
`; exports[`Card shows Card with default props 1`] = ` -.c0 { - position: relative; -} - .c2 { pointer-events: none; position: absolute; @@ -40,7 +64,8 @@ exports[`Card shows Card with default props 1`] = ` left: 0; } -.c1 { +.c0 { + cursor: pointer; display: -webkit-inline-box; display: -webkit-inline-flex; display: -ms-inline-flexbox; @@ -51,37 +76,127 @@ exports[`Card shows Card with default props 1`] = ` font-size: 1rem; border-radius: 0.25rem; border: 0px solid transparent; - -webkit-transition: filter .5s,box-shadow .5s,border .3s; - transition: filter .5s,box-shadow .5s,border .3s; + -webkit-transition: filter .5s, box-shadow .5s, border .3s, background-color .3s; + transition: filter .5s, box-shadow .5s, border .3s, background-color .3s; -webkit-filter: drop-shadow(0rem 0.06653090368236622rem 0.25rem rgba(39,47,78,0.4292196217912265)); filter: drop-shadow(0rem 0.06653090368236622rem 0.25rem rgba(39,47,78,0.4292196217912265)); background-color: #fff; } +.c1 { + position: relative; + position: absolute; + width: 100%; + height: 100%; + left: 0; + top: 0; +} +
- + > + +
`; exports[`Card shows Card with non-default elevation 1`] = ` +.c2 { + pointer-events: none; + position: absolute; + top: 0; + left: 0; +} + .c0 { + cursor: pointer; + display: -webkit-inline-box; + display: -webkit-inline-flex; + display: -ms-inline-flexbox; + display: inline-flex; + -webkit-flex-flow: column nowrap; + -ms-flex-flow: column nowrap; + flex-flow: column nowrap; + font-size: 1rem; + border-radius: 0.25rem; + border: 0px solid transparent; + -webkit-transition: filter .5s, box-shadow .5s, border .3s, background-color .3s; + transition: filter .5s, box-shadow .5s, border .3s, background-color .3s; + -webkit-filter: drop-shadow(0rem 0.07538939058881129rem 0.75rem rgba(39,47,78,0.3741399931653331)); + filter: drop-shadow(0rem 0.07538939058881129rem 0.75rem rgba(39,47,78,0.3741399931653331)); + background-color: #fff; +} + +.c1 { position: relative; + position: absolute; + width: 100%; + height: 100%; + left: 0; + top: 0; +} + +
+
+
+ +
+
+
+`; + +exports[`Card shows Card with ripple feedback and no onClick 1`] = ` +.c0 { + display: -webkit-inline-box; + display: -webkit-inline-flex; + display: -ms-inline-flexbox; + display: inline-flex; + -webkit-flex-flow: column nowrap; + -ms-flex-flow: column nowrap; + flex-flow: column nowrap; + font-size: 1rem; + border-radius: 0.25rem; + border: 0px solid transparent; + -webkit-transition: filter .5s, box-shadow .5s, border .3s, background-color .3s; + transition: filter .5s, box-shadow .5s, border .3s, background-color .3s; + -webkit-filter: drop-shadow(0rem 0.06653090368236622rem 0.25rem rgba(39,47,78,0.4292196217912265)); + filter: drop-shadow(0rem 0.06653090368236622rem 0.25rem rgba(39,47,78,0.4292196217912265)); + background-color: #fff; } +
+
+
+`; + +exports[`Card shows Card with ripple feedback with onClick 1`] = ` .c2 { pointer-events: none; position: absolute; @@ -89,7 +204,8 @@ exports[`Card shows Card with non-default elevation 1`] = ` left: 0; } -.c1 { +.c0 { + cursor: pointer; display: -webkit-inline-box; display: -webkit-inline-flex; display: -ms-inline-flexbox; @@ -100,34 +216,73 @@ exports[`Card shows Card with non-default elevation 1`] = ` font-size: 1rem; border-radius: 0.25rem; border: 0px solid transparent; - -webkit-transition: filter .5s,box-shadow .5s,border .3s; - transition: filter .5s,box-shadow .5s,border .3s; - -webkit-filter: drop-shadow(0rem 0.07538939058881129rem 0.75rem rgba(39,47,78,0.3741399931653331)); - filter: drop-shadow(0rem 0.07538939058881129rem 0.75rem rgba(39,47,78,0.3741399931653331)); + -webkit-transition: filter .5s, box-shadow .5s, border .3s, background-color .3s; + transition: filter .5s, box-shadow .5s, border .3s, background-color .3s; + -webkit-filter: drop-shadow(0rem 0.06653090368236622rem 0.25rem rgba(39,47,78,0.4292196217912265)); + filter: drop-shadow(0rem 0.06653090368236622rem 0.25rem rgba(39,47,78,0.4292196217912265)); background-color: #fff; } +.c1 { + position: relative; + position: absolute; + width: 100%; + height: 100%; + left: 0; + top: 0; +} +
- + > + +
`; -exports[`Card shows Card with non-default feedback 1`] = ` +exports[`Card shows Card with simple feedback and no onClick 1`] = ` +.c0 { + display: -webkit-inline-box; + display: -webkit-inline-flex; + display: -ms-inline-flexbox; + display: inline-flex; + -webkit-flex-flow: column nowrap; + -ms-flex-flow: column nowrap; + flex-flow: column nowrap; + font-size: 1rem; + border-radius: 0.25rem; + border: 0px solid transparent; + -webkit-transition: filter .5s, box-shadow .5s, border .3s, background-color .3s; + transition: filter .5s, box-shadow .5s, border .3s, background-color .3s; + -webkit-filter: drop-shadow(0rem 0.06653090368236622rem 0.25rem rgba(39,47,78,0.4292196217912265)); + filter: drop-shadow(0rem 0.06653090368236622rem 0.25rem rgba(39,47,78,0.4292196217912265)); + background-color: #fff; +} + +
+
+
+`; + +exports[`Card shows Card with simple feedback with onClick 1`] = ` .c0 { + cursor: pointer; display: -webkit-inline-box; display: -webkit-inline-flex; display: -ms-inline-flexbox; @@ -138,13 +293,17 @@ exports[`Card shows Card with non-default feedback 1`] = ` font-size: 1rem; border-radius: 0.25rem; border: 0px solid transparent; - -webkit-transition: filter .5s,box-shadow .5s,border .3s; - transition: filter .5s,box-shadow .5s,border .3s; + -webkit-transition: filter .5s, box-shadow .5s, border .3s, background-color .3s; + transition: filter .5s, box-shadow .5s, border .3s, background-color .3s; -webkit-filter: drop-shadow(0rem 0.06653090368236622rem 0.25rem rgba(39,47,78,0.4292196217912265)); filter: drop-shadow(0rem 0.06653090368236622rem 0.25rem rgba(39,47,78,0.4292196217912265)); background-color: #fff; } +.c0:active { + background-color: #e6e6e6; +} +