Skip to content

Commit

Permalink
Merge pull request #144 from oaknational/feat/PupilJourneyListItem
Browse files Browse the repository at this point in the history
PUPIL-585 Feat/pupil journey list item
  • Loading branch information
benprotheroe authored Apr 17, 2024
2 parents b175cdc + b07c028 commit b85ff7a
Show file tree
Hide file tree
Showing 5 changed files with 665 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import React from "react";
import { Meta, StoryObj } from "@storybook/react";

import { OakPupilJourneyListItem } from "./OakPupilJourneyListItem";

import { OakFlex } from "@/components/atoms";

const meta: Meta<typeof OakPupilJourneyListItem> = {
component: OakPupilJourneyListItem,
tags: ["autodocs"],
title: "components/organisms/pupil/OakPupilJourneyListItem",
args: {
title: "Lesson 1",
index: 1,
href: "#",
},
argTypes: {
title: { control: { type: "text" } },
index: { control: { type: "number" } },
numberOfLessons: { control: { type: "number" } },
disabled: { control: { type: "boolean" } },
unavailable: { control: { type: "boolean" } },
},
decorators: [
(Story) => {
return (
<OakFlex
$flexDirection="column"
$gap="space-between-m"
$background={"bg-decorative4-main"}
$pa={"inner-padding-xl"}
>
{Story()}
</OakFlex>
);
},
],
parameters: {
controls: {
include: ["title", "index", "numberOfLessons", "disabled", "unavailable"],
},
},
};
export default meta;

type Story = StoryObj<typeof OakPupilJourneyListItem>;

export const Default: Story = {
render: (args) => <OakPupilJourneyListItem {...args} />,
};

export const Lessons: Story = {
render: (args) => <OakPupilJourneyListItem {...args} />,
args: {
numberOfLessons: 6,
},
};

export const ReallyLongTitle: Story = {
render: (args) => <OakPupilJourneyListItem {...args} />,
args: {
title:
"This is a really long title that should wrap around to the next line",
numberOfLessons: 6,
},
};

export const AsAButton: Story = {
render: (args) => <OakPupilJourneyListItem {...args} as="button" />,
args: {
href: undefined,
},
};

export const Disabled: Story = {
render: (args) => <OakPupilJourneyListItem {...args} />,
args: {
disabled: true,
},
};

export const Unavailable: Story = {
render: (args) => <OakPupilJourneyListItem {...args} />,
args: {
disabled: true,
unavailable: true,
},
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import React from "react";
import "@testing-library/jest-dom";
import { create } from "react-test-renderer";

import { OakPupilJourneyListItem } from "./OakPupilJourneyListItem";

import { OakThemeProvider } from "@/components/atoms";
import { oakDefaultTheme } from "@/styles";
import renderWithTheme from "@/test-helpers/renderWithTheme";

describe(OakPupilJourneyListItem, () => {
it("matches snapshot", () => {
const tree = create(
<OakThemeProvider theme={oakDefaultTheme}>
<OakPupilJourneyListItem as="a" title="Lesson 1" index={1} />,
</OakThemeProvider>,
).toJSON();

expect(tree).toMatchSnapshot();
});

it("renders a div when the item is disabled", () => {
const { getByTestId } = renderWithTheme(
<>
<OakPupilJourneyListItem
data-testid="intro"
as="a"
title="Lesson 1"
index={1}
disabled
/>
</>,
);

expect(getByTestId("intro").tagName).toBe("DIV");
});
it("renders a button when the item is not disabled", () => {
const { getByTestId } = renderWithTheme(
<>
<OakPupilJourneyListItem
data-testid="intro"
as="a"
title="Lesson 1"
index={1}
/>
</>,
);

expect(getByTestId("intro").tagName).toBe("A");
});
it("renders the number of lessons when provided", () => {
const { getByTestId } = renderWithTheme(
<>
<OakPupilJourneyListItem
data-testid="intro"
as="a"
title="Lesson 1"
index={1}
numberOfLessons={6}
/>
</>,
);

expect(getByTestId("intro").textContent).toContain("6");
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
import React, { ComponentPropsWithoutRef, ElementType } from "react";
import styled, { css } from "styled-components";

import { OakBox, OakFlex } from "@/components/atoms";
import { parseColor } from "@/styles/helpers/parseColor";
import { OakRoundIcon } from "@/components/molecules";
import { parseColorFilter } from "@/styles/helpers/parseColorFilter";
import { parseSpacing } from "@/styles/helpers/parseSpacing";
import { parseDropShadow } from "@/styles/helpers/parseDropShadow";

type OakPupilJourneyListItemProps<C extends ElementType> = {
as?: C;
/**
* Disable the section preventing navigation to it.
*/
disabled?: boolean;
/**
* shows that a section is unavailable
*/
unavailable?: boolean;
index: number;
title: string;
numberOfLessons?: number;
} & ComponentPropsWithoutRef<C>;

const StyledLabel = styled(OakBox)``;

const StyledRoundIcon = styled(OakRoundIcon)<{
$disabled?: boolean;
}>`
width: ${parseSpacing("all-spacing-8")};
height: ${parseSpacing("all-spacing-8")};
padding: 0;
background: transparent;
img {
filter: ${(props) =>
parseColorFilter(props.$disabled ? "icon-disabled" : "icon-inverted")};
}
`;

const activeIconStyles = css`
${StyledRoundIcon} {
box-shadow: ${parseDropShadow("drop-shadow-lemon")},
${parseDropShadow("drop-shadow-grey")};
}
`;

const hoverIconStyles = css`
${StyledRoundIcon} {
background: ${parseColor("bg-btn-primary")};
img {
filter: ${parseColorFilter("icon-main")};
}
}
`;

const StyledLessonNavItem = styled(OakFlex)<{ $disabled?: boolean }>`
outline: none;
text-align: initial;
&:focus-visible {
box-shadow: ${parseDropShadow("drop-shadow-centered-lemon")},
${parseDropShadow("drop-shadow-centered-grey")};
}
${(props) => props.$disabled && "cursor: not-allowed"}
${(props) =>
!props.$disabled &&
css`
cursor: pointer;
/* Don't apply hover styles on touch devices */
@media (hover: hover) {
&:hover {
background: ${parseColor("bg-decorative1-subdued")};
${StyledLabel} {
text-decoration: underline;
}
${hoverIconStyles}
}
}
&:active {
box-shadow: ${parseDropShadow("drop-shadow-lemon")},
${parseDropShadow("drop-shadow-grey")};
${activeIconStyles}
${hoverIconStyles}
}
`}
`;

const FlexedOakBox = styled(OakBox)`
flex: 1;
`;

/**
* Enables navigation to the given section of a lesson as well as displaying current progress
*/
export const OakPupilJourneyListItem = <C extends ElementType = "a">(
props: OakPupilJourneyListItemProps<C>,
) => {
const {
as,
lessonSectionName,
progress,
disabled,
href,
unavailable,
onClick,
...rest
} = props;

const disabledOrUnavailable = disabled || unavailable;
return (
<StyledLessonNavItem
as={disabledOrUnavailable ? "div" : as ?? "a"}
$gap={["space-between-s", "space-between-m2"]}
$alignItems="center"
$justifyContent={"space-between"}
$flexWrap={"wrap"}
$background={unavailable ? "bg-neutral" : "bg-primary"}
$pa={["inner-padding-l", "inner-padding-xl"]}
$borderRadius="border-radius-m"
$ba={unavailable ? "border-solid-m" : "border-solid-none"}
$borderColor={unavailable ? "border-neutral-lighter" : "transparent"}
$disabled={disabledOrUnavailable}
$color="text-primary"
href={disabledOrUnavailable ? undefined : href}
onClick={disabledOrUnavailable ? undefined : onClick}
{...rest}
>
<OakFlex $alignItems={"center"} $gap={["space-between-m2"]}>
{" "}
<OakFlex>
<OakBox
$font={["heading-5", "heading-4"]}
$color={props.unavailable ? "text-subdued" : "text-primary"}
$textDecoration={"none"}
>
{props.index}
</OakBox>
</OakFlex>
<FlexedOakBox>
<StyledLabel
$font={["heading-6", "heading-5"]}
$color={disabledOrUnavailable ? "text-subdued" : "text-primary"}
>
{props.title}
</StyledLabel>
</FlexedOakBox>
</OakFlex>

<OakFlex
$alignItems={"center"}
$gap={"space-between-xs"}
$flexBasis={"auto"}
$flexGrow={1}
$justifyContent={"flex-end"}
>
{props.numberOfLessons && !props.unavailable && (
<StyledLabel
$font={"heading-7"}
$color={disabledOrUnavailable ? "text-subdued" : "text-primary"}
>
{props.numberOfLessons} lessons
</StyledLabel>
)}
{props.unavailable && (
<StyledLabel
$font={"heading-7"}
$color={disabledOrUnavailable ? "text-subdued" : "text-primary"}
>
Unavailable
</StyledLabel>
)}
{!props.unavailable && (
<StyledRoundIcon
iconName="chevron-right"
$disabled={disabledOrUnavailable}
/>
)}
</OakFlex>
</StyledLessonNavItem>
);
};
Loading

0 comments on commit b85ff7a

Please sign in to comment.