Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(components): Carousel and Step components #482

Merged
merged 16 commits into from
Jan 28, 2020

Conversation

voronianski
Copy link
Member

@voronianski voronianski commented Sep 28, 2019

Closes #409.

Approach and changes

  • add generic Step component and hook for managing state in step/index based UIs and components
  • add generic AspectRatio component for keeping proper image proportions
  • add stateful version Carousel component powered by Step and AspectRatio components
  • add smaller blocks of the Carousel like Buttons, Progress, Status, Slide, etc. for composing them in a Custom carousel experience

Step component

Technically component uses the "function as child" and "prop getter" patterns but it's also possible to use it just as a hook:

import { Step } from '@sumup/circuit-ui';

const config = { ... };

<Step {...config}>
  {({ state, actions, ...propGetters }) => {
    // render your component here
  }}
</Step>

vs.

import { useStep } from '@sumup/circuit-ui';

const config = { ... };
const { state, actions, ...propGetters } = useStep(config);

Here are the examples below of how Step component can be used in a different ways to build index-based UIs.

Simple image slider

step-image-slider

import { Step, Button, ButtonGroup, Image } from '@sumup/circuit-ui';

const figureStyles = ({ step, animationDuration }) => css`
  transform: translate3d(${-step * 300}px, 0, 0);
  transition: all ${animationDuration}ms ease-in-out;
`;
const Figure = styled('figure')(figureStyles);

const ImagelSlider = ({ images = [] }) => (
  <Step totalSteps={images.length}>
    {({
      state,
      getNextControlProps,
      getPreviousControlProps,
      getPauseControlProps,
      getPlayControlProps
    }) => (
      <div>
        <Figure
          step={state.step}
          animationDuration={state.animationDuration}
        >
          {images.map((image, i) => (
            <Image
              key={i}
              src={image}
              alt="random pic"
            />
          ))}
        </Figure>
        <ButtonGroup>
          <Button {...getPreviousControlProps()}>Prev</Button>
          <Button
            primary
            {...(state.paused ? getPlayControlProps() : getPauseControlProps())}
          >
            {state.paused ? 'Play' : 'Pause'}
          </Button>
          <Button {...getNextControlProps()}>Next</Button>
        </ButtonGroup>
      </div>
    )}
  </Step>
);

Multistep form

Step is suitable not only for image sliders. Any multi-step guides or slideshows can be achieved with it too:

step-form

import { Step, Label, Input, Select, Button, ButtonGroup, Heading, ProgressBar } from '@sumup/circuit-ui';

const FormOne = ({ onNextClick }) => (
  <section>
    <Label htmlFor="first">First Name</Label>
    <Input placeholder="John" id="first" />
    <Label htmlFor="second">Second Name</Label>
    <Input placeholder="Doe" id="second" />
    <Button primary onClick={() => onNextClick()}>
      Next
    </Button>
  </section>
);

const FormTwo = ({ onNextClick, onBackClick }) => (
  <section>
    <Label htmlFor="street">Street</Label>
    <Input placeholder="Madison Ave 5" id="street" />
    <Label htmlFor="state">State</Label>
    <Select id="state">
      <option>CA</option>
      <option>TX</option>
      <option>NY</option>
    </Select>
    <Label htmlFor="postal">Postal Code</Label>
    <Input placeholder="10179" id="postal" />
    <ButtonGroup align={ButtonGroup.LEFT}>
      <Button primary onClick={() => onNextClick()}>
        Submit
      </Button>
      <Button onClick={() => onBackClick()}>Back</Button>
    </ButtonGroup>
  </section>
);

const Thanks = () => (
  <section>
    <Heading>Thanks!</Heading>
  </section>
);

const MultiStepForm = () => {
  const steps = [FormOne, FormTwo, Thanks];
  const totalSteps = steps.length;

  return (
    <Step total={totalSteps}>
      {({ state, actions }) => {
        const StepComponent = steps[state.step];
        const stepNumber = state.step + 1;

        return (
          <div>
            <Heading size={Heading.GIGA}>
              Step {stepNumber} of {totalSteps}
            </Heading>
            <ProgressBar
              value={stepNumber}
              max={totalSteps}
              size={ProgressBar.KILO}
            />
            <StepComponent
              onNextClick={actions.next}
              onBackClick={actions.previous}
            />
          </div>
        );
      }}
    </Step>
  );
};

Yes/No slider with react-swipeable

And here is the interesting example of how you can easily add swipe/touch support into your slider-like UI if it's necessary:

step-yes-no

import { Swipeable } from 'react-swipeable';
import { Step, Button, ButtonGroup } from '@sumup/circuit-ui';

const YesOrNoSlider = ({ images = [] }) => {
  const [swipe, setSwipe] = useState({});
  const handleSwipe = (eventData, actions) => {
    setSwipe(eventData);

    if (eventData.dir === LEFT_DIRECTION) {
      actions.previous();
    }

    if (eventData.dir === RIGHT_DIRECTION) {
      actions.next();
    }
  };

  return (
    <Step totalSteps={images.length}>
      {({ state, actions, getPreviousControlProps, getNextControlProps }) => (
        <div>
          <Swipeable onSwiped={eventData => handleSwipe(eventData, actions)}>
            <SliderImage
              src={images[state.step]}
              alt="random pic"
              swipe={swipe}
            />
          </Swipeable>
          <ButtonGroup align={ButtonGroup.CENTER}>
            <Button
              primary={swipe.dir === LEFT_DIRECTION}
              {...getPreviousControlProps()}
            >
              No
            </Button>
            <Button
              primary={swipe.dir === RIGHT_DIRECTION}
              {...getNextControlProps()}
            >
              Yes
            </Button>
          </ButtonGroup>
        </div>
      )}
    </Step>
  );
};

Carousel component

Carousel is a port of component designed for the website and you can also see it in action as part of Testimonials component on careers page (more details in proposal - #409).

So currently there are 2 ways of using it.

Stateful

First one is just as simple as passing array of images into it. All internal state management is handled by Step component automatically:

carousel-stateful

import { Carousel } from '@sumup/circuit-ui';

const slides = [
  {  image: { src, alt } }, 
  {  image: { src, alt } }
  //...
];

<Carousel slides={slides} />

Composed

Second approach is compositional which means you get the building blocks and arrange them in a way that you want. Keep in mind that you will need to manage state by yourself (or via Step):

carousel-composed

import { CarouselComposed } from '@sumup/circuit-ui';

const { 
  Container, 
  Slides, 
  Slide, 
  SlideImage, 
  Controls, 
  Status,
  PrevButton,
  NextButton
} = CarouselComposed

const slides = [
  {  image: { src, alt } }, 
  {  image: { src, alt } }
  //...
];

const CustomCarousel = () => {
  const total = slides.length;
  const [step, setStep] = useState(0);
  const goBack = () => setStep(step === 0 ? total - 1 : step - 1);
  const goForward = () => setStep(step === total - 1 ? 0 : step + 1);

  return (
    <Container>
      <Slides>
        {slides.map(({ image }, index) => (
          <Slide
            key={index}
            index={index}
            step={step}
          >
            <SlideImage
              src={image.src}
              alt={image.alt}
            />
          </Slide>
        ))}
      </Slides>
      <Controls>
        <PrevButton onClick={goBack} />
        <Status step={step} total={total}  />
        <NextButton onClick={goForward} />
      </Controls>
    </Container>
  );
};

Limitations

As I mentioned before Step is just purely state management abstraction and it doesn't contain any touch/swipe event support into it. I also decided not to introduce any 3rd party "swipeable" component dependency for now. This means that currently Carousel component doesn't support swiping of images on mobiles. I see 2 ways of solving it:

  • obviously adding something like react-swipeable as a dependency to Circuit UI
  • creating light Swipe component inside Circuit UI by ourselves (which should be addressed in a separate PR)

I leave it as an open question and your feedback is welcome.

Definition of done

  • Development completed
  • Reviewers assigned
  • Unit and integration tests
  • Meets minimum browser support
  • Meets accessibility requirements

@codecov
Copy link

codecov bot commented Sep 28, 2019

Codecov Report

Merging #482 into alpha will increase coverage by 0.03%.
The diff coverage is 88.88%.

Impacted file tree graph

@@            Coverage Diff             @@
##            alpha     #482      +/-   ##
==========================================
+ Coverage   79.76%   79.79%   +0.03%     
==========================================
  Files         187      188       +1     
  Lines        3251     3256       +5     
  Branches      887      887              
==========================================
+ Hits         2593     2598       +5     
  Misses        542      542              
  Partials      116      116
Impacted Files Coverage Δ
src/util/numbers.js 96.66% <ø> (ø) ⬆️
src/components/Step/StepService.js 100% <ø> (ø) ⬆️
src/util/fp.js 72.41% <0%> (ø) ⬆️
src/components/Carousel/Carousel.js 100% <100%> (+3.03%) ⬆️
src/components/Pagination/PaginationService.js 100% <100%> (ø) ⬆️
src/components/Sidebar/Sidebar.js 100% <100%> (ø) ⬆️
...mponents/Sidebar/components/Separator/Separator.js 100% <100%> (ø)
src/components/Col/utils.js 94.87% <100%> (ø) ⬆️
src/util/currency.js 100% <100%> (ø) ⬆️
.../components/Carousel/components/Buttons/Buttons.js 100% <100%> (ø) ⬆️
... and 3 more

Copy link
Member

@connor-baer connor-baer left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I love how clean and flexible this code is! Beautifully done ✨

What did you think of the new testing setup?

Copy link
Contributor

@fernandofleury fernandofleury left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for pushing it through, and thanks for also providing such in depth examples. I just wrote down some small ideas.

import Carousel from './Carousel';
import { SLIDES } from './__fixtures__';

describe('Carousel', () => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't we also add functional tests to the Carousel?

Like:

  • clicking the next slide and expecting it to be the active one
  • clicking on the last slide and expecting it to loop
  • waiting for the timer to change the active slide

Copy link
Member Author

@voronianski voronianski Jan 24, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yo @fernandofleury, thanks for suggestion, however all the points you mentioned are covered by the tests in useStep hook which is used inside Carousel component and is responsible for managing the state - https://github.com/sumup-oss/circuit-ui/blob/feature/add-carousel-and-step-components/src/components/Step/hooks/use-step.spec.js

Also all possible variations of internal Carousel component styles are tested separately inside components itself by passing necessary props and matching the snapshots (e.g. Slide component and its' different animations).

All components in Step and Carousel have 100% code coverage.

I agree that having functional/e2e test is nice addition but in current setup (react-testing-library) we will end up with just testing React onClick handlers calling the right functions. This is totally fine but it also feels like having too match hassle (e.g. adding a lot of data-selectors) for no real benefit.

WDYT?


const slideIn = keyframes`
from {
width: 0%;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it possible to use transform: scaleX instead of width? This way the animation can be optimised by the GPU.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it's possible but animation doesn't look the way it should be unfortunately.

carousel-transform-scalex

Comment on lines +64 to +70
${isAnimating &&
css`
animation-name: ${animationName};
animation-duration: ${animationDuration}ms;
animation-fill-mode: forwards;
animation-timing-function: ${theme.transitions.easeInOutCubic};
`};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
${isAnimating &&
css`
animation-name: ${animationName};
animation-duration: ${animationDuration}ms;
animation-fill-mode: forwards;
animation-timing-function: ${theme.transitions.easeInOutCubic};
`};
animation-name: ${animationName};
animation-duration: ${animationDuration}ms;
animation-fill-mode: forwards;
animation-timing-function: ${theme.transitions.easeInOutCubic};
animation-play-state: ${isAnimating ? 'running' : 'paused'}

Maybe we can use the play-state here? This way you don't have to conditionally add rules, just one value

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Due to the way animation-play-state prop works, it leads to inconsistent sliding animations -

Resuming a paused animation will start the animation from where it left off at the time it was paused, rather than starting over from the beginning of the animation sequence.

@voronianski voronianski changed the base branch from canary to alpha November 15, 2019 09:16
@connor-baer
Copy link
Member

@voronianski What's the state of this PR? Could you please address the review comments so we can merge it soon? Is there anything I can help you with? :)

@voronianski
Copy link
Member Author

@voronianski What's the state of this PR? Could you please address the review comments so we can merge it soon? Is there anything I can help you with? :)

@connor-baer, thanks a lot but no help is needed, it's just a question of time :) will address this asap

@vercel
Copy link

vercel bot commented Jan 24, 2020

This pull request is being automatically deployed with ZEIT Now (learn more).
To see the status of your deployment, click below or on the icon next to each commit.

🔍 Inspect: https://zeit.co/sumup-oss/circuit-ui/1zg2bhx28
✅ Preview: https://circuit-ui-git-feature-add-carousel-and-step-components.sumup-oss.now.sh

@voronianski
Copy link
Member Author

voronianski commented Jan 24, 2020

yo @connor-baer, I addressed all the comments, feel free to recheck the PR.

Copy link
Member

@connor-baer connor-baer left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I loooooove the work you've put into this pull request. So much attention to detail! Also kudos for keeping it in sync with the many upstream changes 👏👏

Let's merge this baby!

@voronianski voronianski merged commit 979a36f into alpha Jan 28, 2020
@voronianski voronianski deleted the feature/add-carousel-and-step-components branch January 28, 2020 15:26
@github-actions
Copy link
Contributor

🎉 This PR is included in version 2.0.0-alpha.2 🎉

The release is available on:

Your semantic-release bot 📦🚀

@larimaza
Copy link
Contributor

Can we close #409? @connor-baer

@github-actions
Copy link
Contributor

github-actions bot commented Jun 8, 2020

🎉 This PR is included in version 2.0.0-beta.1 🎉

The release is available on:

Your semantic-release bot 📦🚀

@github-actions
Copy link
Contributor

🎉 This PR is included in version 2.0.0 🎉

The release is available on:

Your semantic-release bot 📦🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants