Skip to content

Commit

Permalink
feat: add content section component
Browse files Browse the repository at this point in the history
  • Loading branch information
petermakowski committed Dec 1, 2023
1 parent 981b6bd commit 6a7d717
Show file tree
Hide file tree
Showing 10 changed files with 224 additions and 19 deletions.
22 changes: 22 additions & 0 deletions src/lib/sections/ContentSection/ContentSection.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
@import "vanilla-framework";

$section-padding: 1.5rem;

section.content-section {
padding: $section-padding 0 0;

.content-section__footer {
background: white;
width: 100%;
position: sticky;
margin-top: $section-padding;
bottom: 0;
border-top: $border;
padding: 1rem 0;

// Reduce the margin of all direct descendants for consistent spacing
> * {
margin-bottom: 0;
}
}
}
57 changes: 57 additions & 0 deletions src/lib/sections/ContentSection/ContentSection.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { Button } from "@canonical/react-components";
import { Meta } from "@storybook/react";

import { ContentSection } from "@/lib/sections/ContentSection/ContentSection";
import { WithInputGroup } from "@/lib/sections/FormSection/FormSection.stories";

const meta: Meta<typeof ContentSection> = {
title: "sections/ContentSection",
component: ContentSection,
tags: ["autodocs"],
argTypes: {
as: {
control: "text",
},
children: {
control: {
disable: true,
},
},
},
render: (args) => (
// fixed height container to demonstrate sticky footer
<div style={{ height: "250px", overflowY: "auto" }}>
<ContentSection {...args} />
</div>
),
};

export default meta;

export const Example = {
args: {
children: (
<>
<ContentSection.Title>Section Title</ContentSection.Title>
<ContentSection.Content>Section Content</ContentSection.Content>
<ContentSection.Footer>Section Footer</ContentSection.Footer>
</>
),
},
};

export const WithForm = {
args: {
children: (
<>
<ContentSection.Title>Section Title</ContentSection.Title>
<ContentSection.Content>
<WithInputGroup.render />
</ContentSection.Content>
<ContentSection.Footer>
<Button appearance="positive">Submit</Button>
</ContentSection.Footer>
</>
),
},
};
53 changes: 53 additions & 0 deletions src/lib/sections/ContentSection/ContentSection.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { render, screen } from "@testing-library/react";

import { ContentSection } from ".";

it("renders all elements correctly", () => {
render(
<ContentSection>
<ContentSection.Title>Test Title</ContentSection.Title>
<ContentSection.Content>Test Content</ContentSection.Content>
<ContentSection.Footer>Test Footer</ContentSection.Footer>
</ContentSection>,
);
expect(
screen.getByRole("heading", { name: "Test Title" }),
).toBeInTheDocument();
expect(screen.getByText("Test Content")).toBeInTheDocument();
expect(screen.getByText("Test Footer")).toBeInTheDocument();
});

it("renders custom classNames", () => {
render(
<ContentSection className="custom-content-section-class">
<ContentSection.Title className="custom-title-class">
Test Title
</ContentSection.Title>
<ContentSection.Content className="custom-content-class">
Test Content
</ContentSection.Content>
<ContentSection.Footer className="custom-footer-class">
Test Footer
</ContentSection.Footer>
</ContentSection>,
);
expect(
document.querySelector(".custom-content-section-class"),
).toBeInTheDocument();
expect(document.querySelector(".custom-title-class")).toBeInTheDocument();
expect(document.querySelector(".custom-content-class")).toBeInTheDocument();
expect(document.querySelector(".custom-footer-class")).toBeInTheDocument();
});

it("renders custom element for the title", () => {
render(
<ContentSection as="section">
<ContentSection.Title as="h5">Test Title</ContentSection.Title>
<ContentSection.Content>Test Content</ContentSection.Content>
<ContentSection.Footer>Test Footer</ContentSection.Footer>
</ContentSection>,
);
expect(
screen.getByRole("heading", { level: 5, name: "Test Title" }),
).toBeInTheDocument();
});
69 changes: 69 additions & 0 deletions src/lib/sections/ContentSection/ContentSection.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import * as React from "react";

import { Col } from "@canonical/react-components";
import classNames from "classnames";

import "./ContentSection.scss";
import { AsProp } from "@/types";

interface CommonContentSectionProps extends React.PropsWithChildren {
className?: string;
align?: "left" | "center" | "right";
}
export interface ContentSectionProps
extends CommonContentSectionProps,
React.HTMLAttributes<HTMLElement>,
AsProp<React.ElementType>,
React.PropsWithChildren {}

/**
* A content section layout component for one of the primary content areas (e.g. main or sidebar).
*
* `ContentSection` has three child components:
* - `ContentSection.Title`
* - `ContentSection.Content`
* - `ContentSection.Footer`
*
* `ContentSection.Footer` is made sticky by default.
*/
export const ContentSection = ({
children,
className,
as,
...props
}: ContentSectionProps) => {
const Component = as || "section";
return (
<Component {...props} className={classNames("content-section", className)}>
<Col size={12}>{children}</Col>
</Component>
);
};

const Title = ({ children, className, as, ...props }: ContentSectionProps) => {
const Component = as || "h1";
return (
<Component
{...props}
className={classNames("content-section__title p-heading--4", className)}
>
{children}
</Component>
);
};

const Content = ({ children, className }: CommonContentSectionProps) => (
<div className={classNames("content-section__body", className)}>
{children}
</div>
);

const Footer = ({ children, className }: CommonContentSectionProps) => (
<div className={classNames("content-section__footer", className)}>
{children}
</div>
);

ContentSection.Title = Title;
ContentSection.Content = Content;
ContentSection.Footer = Footer;
1 change: 1 addition & 0 deletions src/lib/sections/ContentSection/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./ContentSection";
2 changes: 1 addition & 1 deletion src/lib/sections/Navigation/Link/Link.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { ComponentProps, ElementType } from "react";

import classNames from "classnames";

import { AsProp } from "@/lib/sections/Navigation/types";
import { AsProp } from "@/types";

export interface NavigationLinkProps extends ComponentProps<typeof Link> {}

Expand Down
23 changes: 13 additions & 10 deletions src/lib/sections/Navigation/Logo/Logo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,25 @@ import { ComponentProps, ElementType, ReactNode } from "react";

import classNames from "classnames";

import { AsProp } from "@/lib/sections/Navigation/types";
import { AsProp } from "@/types";

export interface NavigationLogoProps extends ComponentProps<typeof Logo> {
children: ReactNode;
}

export const Logo = <
C extends ElementType = "a",
T extends ComponentProps<C> = ComponentProps<C>,
>({ as, children, className, ...props }: AsProp<C> & Omit<T, "as">) => {
C extends ElementType = "a",
T extends ComponentProps<C> = ComponentProps<C>,
>({
as,
children,
className,
...props
}: AsProp<C> & Omit<T, "as">) => {
const Component = as || "a";
return (
return (
<Component className={classNames("p-panel__logo", className)} {...props}>
<div className="p-navigation__tagged-logo">
{children}
</div>
<div className="p-navigation__tagged-logo">{children}</div>
</Component>
)
}
);
};
5 changes: 0 additions & 5 deletions src/lib/sections/Navigation/types.ts

This file was deleted.

5 changes: 2 additions & 3 deletions src/lib/sections/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
export * from "./Navigation";

export * from "./InputGroup";
export * from "./FormSection";

export * from "./MainToolbar";
export * from "./MainToolbar";
export * from "./ContentSection";
6 changes: 6 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
import { ElementType } from "react";

export type Prettify<T> = {
[K in keyof T]: T[K];
// eslint-disable-next-line @typescript-eslint/ban-types
} & {};

export interface AsProp<C extends ElementType> {
as?: C;
}

0 comments on commit 6a7d717

Please sign in to comment.