Skip to content

Commit

Permalink
feat(lld): 🎠 update react-ui default carousel design (#8371)
Browse files Browse the repository at this point in the history
* feat(lld): add arrows to the default carousel

* feat(lld): update the default carousel bullets

* feat(lld): hide carousel controls for a single slide

* fix(lld): carousel styling

* chore(lld): improve the carousel story

* chore: update change log

* chore(lld): add handlers for the ChevronArrow onClick functions

* fix(lld): storybook's light theme background

* fix(lld): do not set a background color for the carousel

* chore: set the `@ledgerhq/ui-shared` change to minor instead of patch
  • Loading branch information
thesan authored Nov 20, 2024
1 parent ccb0f6c commit be83cab
Show file tree
Hide file tree
Showing 11 changed files with 137 additions and 30 deletions.
6 changes: 6 additions & 0 deletions .changeset/orange-ducks-do.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@ledgerhq/react-ui": minor
"@ledgerhq/ui-shared": minor
---

Update the react-ui Carousel based on the portfolio content cards design
6 changes: 3 additions & 3 deletions libs/ui/packages/react/.storybook/preview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { StyleProvider } from "../src/styles";
export const decorators = [
(Story, { globals }) => {
const backgrounds = globals?.backgrounds ?? {};
const theme = backgrounds?.value === palettes.dark.background.main ? "dark" : "light";
const theme = backgrounds?.value === palettes.dark.background.default ? "dark" : "light";
return (
<StyleProvider fontsPath="assets/fonts" selectedPalette={theme}>
<Story />
Expand All @@ -29,11 +29,11 @@ export const parameters = {
values: [
{
name: "light",
value: palettes.light.background.main,
value: palettes.light.background.default,
},
{
name: "dark",
value: palettes.dark.background.main,
value: palettes.dark.background.default,
},
],
},
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import type { Meta, StoryObj } from "@storybook/react";
import React from "react";
import Carousel from "./";
import { Props } from "./types";

const CarouselStory = (args: Props) => {
const slides = [...Array(5)].map((_, index) => (
const CarouselStory = (args: Omit<Props, "children"> & { children: number }) => {
const slides = Array.from({ length: args.children }, (_, index) => (
<div
key={index}
style={{
backgroundColor: "hsl(" + Math.random() * 360 + ", 100%, 75%)",
padding: "15px",
padding: "16px 24px",
borderRadius: "5px",
}}
>
Expand All @@ -24,16 +25,18 @@ export default {
argTypes: {
children: {
description: "The elements to be displayed.",
control: { type: "range", min: 1, max: 10, step: 1 },
},
variant: {
description: "Variant for the carousel.",
options: ["default", "content-card"],
defaultValue: "default",
control: { type: "select" },
control: "inline-radio",
},
},
args: {
variant: "default",
children: 5,
},
parameters: {
docs: {
Expand All @@ -42,6 +45,7 @@ export default {
},
},
},
};
render: CarouselStory,
} satisfies Meta;

export const Default = CarouselStory.bind({});
export const Default: StoryObj = {};
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import React from "react";
import styled from "styled-components";
import { Icons } from "../../../assets";

type Props = ContainerProps & {
onClick: () => void;
};

export function ChevronArrow(props: Props) {
return (
<ChevronArrowContainer {...props}>
<Icons.ChevronLeft />
</ChevronArrowContainer>
);
}

type ContainerProps = {
direction: "left" | "right";
};

const ChevronArrowContainer = styled.button<ContainerProps>`
display: inline-flex;
justify-content: center;
align-items: center;
width: 32px;
height: 32px;
position: absolute;
top: 50%;
left: ${({ direction }) => (direction === "left" ? "0" : "auto")};
right: ${({ direction }) => (direction === "left" ? "auto" : "0")};
z-index: 1;
--dir: ${({ direction }) => (direction === "left" ? "1" : "-1")};
scale: var(--dir) 1;
translate: calc(-50% * var(--dir)) -50%;
background-color: ${({ theme }) => theme.colors.background.default}; // Fake the transparent clip
border-radius: 100%;
border: none;
outline: none;
::before {
content: "";
display: block;
position: absolute;
z-index: -1;
inset: 3px;
background-color: ${({ theme }) => theme.colors.opacityDefault.c05};
border-color: ${({ theme }) => theme.colors.opacityDefault.c05};
border-radius: 100%;
border-style: solid;
border-width: 1px;
}
svg {
color: ${({ theme }) => theme.colors.primary.c100};
}
::before,
svg {
cursor: pointer;
}
transition: opacity 0.2s ease-in-out;
opacity: var(--hover-transition);
::before,
svg {
transition: translate 0.2s ease-in-out;
translate: calc(-50% + 50% * var(--hover-transition)) 0;
}
`;
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ const Pagination = ({ children, currentIndex }: SubProps) => {
return (
<FooterCarouselBullets>
{children.map((child, index) => (
<Bullet key={child.key} type={getItemStatus(index, currentIndex)} />
<Bullet key={child.key} type={getItemStatus(index, currentIndex, children.length)} />
))}
</FooterCarouselBullets>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,17 @@ import { ItemStatus } from "./types";
/**
* Returns the status of an indexed item from the carousel index.
*/
export const getItemStatus = (itemIndex: number, activeIndex: number) => {
const itemDistanceFromActiveIndex = Math.abs(itemIndex - activeIndex);
export const getItemStatus = (itemIndex: number, activeIndex: number, itemCount: number) => {
const isActive = itemIndex === activeIndex;
if (isActive) {
return ItemStatus.active;
}

switch (itemDistanceFromActiveIndex) {
case 0:
return ItemStatus.active;
case 1:
return ItemStatus.nearby;
case 2:
return ItemStatus.far;
default:
return ItemStatus.none;
const isAdjacent = Math.abs(itemIndex - activeIndex) === 1;
if (isAdjacent) {
return ItemStatus.nearby;
}

const isEdge = itemIndex === 0 || itemIndex === itemCount - 1;
return isEdge ? ItemStatus.far : ItemStatus.nearby;
};
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ const Footers: { [key in Variant]: FC<SubProps> } = {
};

const Footer = (props: SubProps) => {
if (props.children.length === 1) return null;

const Component = Footers[props.variant];
return <Component {...props} />;
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,6 @@ const FooterArrowContainer = styled.div`
`;

const FooterContentCard = ({ children, emblaApi, currentIndex, variant }: SubProps) => {
if (children.length === 1) return null;

return (
<FooterContainer>
<Pagination
Expand Down
37 changes: 30 additions & 7 deletions libs/ui/packages/react/src/components/layout/Carousel/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import React, { useCallback, useEffect, useState } from "react";
import styled from "styled-components";
import Footer from "./Footer";
import { Props } from "./types";
import { ChevronArrow } from "./ChevronArrow";

const Embla = styled.div`
overflow: hidden;
Expand All @@ -17,6 +18,15 @@ const EmblaSlide = styled.div`
min-width: 0;
`;

const CarouselContainer = styled.div<Pick<Props, "variant">>`
position: relative;
--hover-transition: 0;
&:hover {
--hover-transition: 1;
}
`;

/**
* This component uses the https://github.com/davidjerleke/embla-carousel library.
*/
Expand Down Expand Up @@ -45,15 +55,27 @@ const Carousel = ({ children, variant = "default" }: Props) => {
emblaApi.on("reInit", updateIndex);
}, [emblaApi, updateIndex]);

const handleGotoPrevSlide = () => emblaApi?.scrollPrev();
const handleGotoNextSlide = () => emblaApi?.scrollNext();

return (
<div>
<Embla ref={emblaRef}>
<EmblaContainer>
{children.map(child => (
<EmblaSlide key={child.key}>{child}</EmblaSlide>
))}
</EmblaContainer>
</Embla>
<CarouselContainer variant={variant}>
{variant === "default" && children.length > 1 && (
<>
<ChevronArrow direction="left" onClick={handleGotoPrevSlide} />
<ChevronArrow direction="right" onClick={handleGotoNextSlide} />
</>
)}

<Embla ref={emblaRef}>
<EmblaContainer>
{children.map(child => (
<EmblaSlide key={child.key}>{child}</EmblaSlide>
))}
</EmblaContainer>
</Embla>
</CarouselContainer>

<Footer
children={children}
Expand All @@ -64,4 +86,5 @@ const Carousel = ({ children, variant = "default" }: Props) => {
</div>
);
};

export default Carousel;
2 changes: 2 additions & 0 deletions libs/ui/packages/shared/palettes/dark.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,8 +107,10 @@ export default {
black: "#000000",
},
background: {
default: "#131214",
main: "#131214",
drawer: "#1D1C1F",
card: "#1C1D1F",
},
effects: {
dropShadow: "rgba(0, 0, 0, 0.48)",
Expand Down
2 changes: 2 additions & 0 deletions libs/ui/packages/shared/palettes/light.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,8 +107,10 @@ export default {
black: "#000000",
},
background: {
default: "#F9F9F9",
main: "#FFFFFF",
drawer: "#FFFFFF",
card: "#FFFFFF",
},
effects: {
dropShadow: "rgba(0, 0, 0, 0.16)",
Expand Down

0 comments on commit be83cab

Please sign in to comment.