Skip to content

Commit

Permalink
Merge pull request #570 from LifeSG/typography-component
Browse files Browse the repository at this point in the history
Set up V3 Typography component
  • Loading branch information
qroll authored Oct 1, 2024
2 parents e11c09c + 60af481 commit 1342b66
Show file tree
Hide file tree
Showing 12 changed files with 518 additions and 1 deletion.
7 changes: 7 additions & 0 deletions .eslintrc
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,13 @@
{
"additionalHooks": "useIsomorphicLayoutEffect"
}
],
"@typescript-eslint/no-unused-vars": [
"error",
{
"argsIgnorePattern": "^_",
"varsIgnorePattern": "^_"
}
]
}
}
1 change: 1 addition & 0 deletions .storybook/preview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ const preview: Preview = {
"Typography",
"Animation",
],
"Core",
"General",
["Animations", "Button", ["Base", "With Icon"], "*"],
"Form",
Expand Down
12 changes: 12 additions & 0 deletions src/theme/typography/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,18 @@ export type TypographyCollectionMap = {

export type TypographySetOptions = Partial<TypographySet>;

export type TypographySizeType =
| "header-xxl"
| "header-xl"
| "header-lg"
| "header-md"
| "header-sm"
| "header-xs"
| "body-baseline"
| "body-lg"
| "body-md"
| "body-sm";

export type TypographySet = {
"header-xxl-light": CSSProp | string;
"header-xxl-regular": CSSProp | string;
Expand Down
42 changes: 42 additions & 0 deletions src/typography/helper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { css } from "styled-components";
import { Colour, Typography } from "../theme";
import { TypographySizeType } from "../theme/typography/types";
import { TypographyProps, TypographyWeight } from "./types";

export const getTextStyle = (
type: TypographySizeType,
weight: TypographyWeight,
paragraph = false
) => {
const token = `${type}-${weight.toLowerCase()}`;

return css`
${Typography[token]}
${paragraph ? "margin-bottom: 1.05em;" : "margin-bottom: 0;"}
`;
};

export const getDisplayStyle = (inline = false, paragraph = false) => {
if (paragraph) {
return css`
display: block;
`;
} else if (inline) {
return css`
display: inline;
`;
} else {
return css`
display: block;
`;
}
};

export const createTypographyStyles = (
textStyle: TypographySizeType,
props: TypographyProps
) => css`
${getTextStyle(textStyle, props.weight || "regular", props.paragraph)}
${getDisplayStyle(props.inline, props.paragraph)}
color: ${Colour.text};
`;
2 changes: 2 additions & 0 deletions src/typography/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from "./typography";
export * from "./types";
18 changes: 18 additions & 0 deletions src/typography/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
export type TypographyWeight = "regular" | "semibold" | "bold" | "light";

export interface TypographyProps extends React.HTMLAttributes<HTMLElement> {
/** The font weight */
weight?: TypographyWeight | undefined;
/** Specifies if text is displayed inline */
inline?: boolean | undefined;
/** Specifies if text has a bottom margin */
paragraph?: boolean | undefined;
}

export interface TypographyLinkProps
extends React.AnchorHTMLAttributes<HTMLAnchorElement> {
/** The font weight */
weight?: TypographyWeight | undefined;
/** Displays indicator to signal that link leads to an external site */
external?: boolean | undefined;
}
88 changes: 88 additions & 0 deletions src/typography/typography.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import { ExternalIcon } from "@lifesg/react-icons/external";
import styled, { css } from "styled-components";
import { Colour } from "../theme";
import { TypographySizeType } from "../theme/typography/types";
import { createTypographyStyles, getTextStyle } from "./helper";
import { TypographyLinkProps, TypographyProps } from "./types";

export namespace Typography {
const createHeader = (
tag: keyof JSX.IntrinsicElements,
textStyle: TypographySizeType,
displayName: string
) => {
const Header = styled(tag).attrs<TypographyProps>(({ inline }) => ({
as: inline ? "span" : undefined,
}))<TypographyProps>`
${(props) => createTypographyStyles(textStyle, props)}
`;
Header.displayName = `Typography.${displayName}`;
return Header;
};

export const HeaderXXL = createHeader("h1", "header-xxl", "HeaderXXL");
export const HeaderXL = createHeader("h2", "header-xl", "HeaderXL");
export const HeaderLG = createHeader("h3", "header-lg", "HeaderLG");
export const HeaderMD = createHeader("h4", "header-md", "HeaderMD");
export const HeaderSM = createHeader("h5", "header-sm", "HeaderSM");
export const HeaderXS = createHeader("h6", "header-xs", "HeaderXS");

const createBody = (textStyle: TypographySizeType, displayName: string) => {
const Body = styled.p.attrs<TypographyProps>(({ inline }) => ({
as: inline ? "span" : undefined,
}))<TypographyProps>`
${(props) => createTypographyStyles(textStyle, props)}
`;
Body.displayName = `Typography.${displayName}`;
return Body;
};

export const BodyBL = createBody("body-baseline", "BodyBL");
export const BodyLG = createBody("body-lg", "BodyLG");
export const BodyMD = createBody("body-md", "BodyMD");
export const BodySM = createBody("body-sm", "BodySM");

const createLinkComponent = (
textStyle: TypographySizeType,
displayName: string
) => {
const HyperlinkBase = styled.a<TypographyLinkProps>`
${(props) => css`
${getTextStyle(textStyle, props.weight || "regular")}
color: ${Colour.hyperlink};
text-decoration: none;
:hover,
:active,
:focus {
color: ${Colour["text-hover"]};
}
`}
`;

const Component = ({
external = false,
children,
...rest
}: TypographyLinkProps) => (
<HyperlinkBase {...rest}>
{children}
{external && <StyledExternalIcon />}
</HyperlinkBase>
);
Component.displayName = `Typography.${displayName}`;
return Component;
};

export const LinkBL = createLinkComponent("body-baseline", "LinkBL");
export const LinkMD = createLinkComponent("body-md", "LinkMD");
export const LinkLG = createLinkComponent("body-lg", "LinkLG");
export const LinkSM = createLinkComponent("body-sm", "LinkSM");
}

const StyledExternalIcon = styled(ExternalIcon)`
height: 1lh;
width: 1em;
margin-left: 0.4em;
vertical-align: middle;
`;
3 changes: 2 additions & 1 deletion stories/storybook-common/api-table/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from "./api-table-components";
export * from "./api-table";
export * from "./api-table-components";
export * from "./markup-helpers";
export * from "./types";
100 changes: 100 additions & 0 deletions stories/typography/props-table.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import {
ApiTable,
ApiTableSectionProps,
TabAttribute,
Tabs,
} from "../storybook-common";

const TEXT_DATA: ApiTableSectionProps[] = [
{
attributes: [
{
name: "",
description: (
<>
This component also inherits props from{" "}
<a
href="https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement"
target="_blank"
rel="noreferrer"
>
HTMLElement
</a>
</>
),
},
{
name: "inline",
description:
"Sets the text to an inline display to allow a combination of text in a single line",
propTypes: ["boolean"],
},
{
name: "maxLines",
description:
"Specifies the number of lines visible. Additional lines will be truncated",
propTypes: ["number"],
},
{
name: "paragraph",
description:
"Adds an extra bottom margin to allow a better separation of text blocks",
propTypes: ["boolean"],
defaultValue: "false",
},
{
name: "weight",
description: "The weight of the text component",
propTypes: [`"regular"`, `"semibold"`, `"bold"`, `"light"`],
defaultValue: `"regular"`,
},
],
},
];

const LINK_DATA: ApiTableSectionProps[] = [
{
attributes: [
{
name: "",
description: (
<>
This component also inherits props from{" "}
<a
href="https://developer.mozilla.org/en-US/docs/Web/API/HTMLAnchorElement"
target="_blank"
rel="noreferrer"
>
HTMLAnchorElement
</a>
</>
),
},
{
name: "weight",
description: "The weight of the hyperlink component",
propTypes: [`"regular"`, `"semibold"`, `"bold"`, `"light"`],
defaultValue: `"regular"`,
},
{
name: "external",
description:
"Indicates if the link is external to the domain. Adds an indicator at the end of the link",
propTypes: ["boolean"],
},
],
},
];

const PROPS_TABLE_DATA: TabAttribute[] = [
{
title: "Header/Body",
component: <ApiTable sections={TEXT_DATA} />,
},
{
title: "Link",
component: <ApiTable sections={LINK_DATA} />,
},
];

export const PropsTable = () => <Tabs tabs={PROPS_TABLE_DATA} />;
80 changes: 80 additions & 0 deletions stories/typography/typography.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { Canvas, Meta } from "@storybook/blocks";
import { DocInfo } from "stories/storybook-common";
import { PropsTable } from "./props-table";
import * as TypographyStories from "./typography.stories";

<Meta of={TypographyStories} />

# Typography

## Overview

The component that is used for headings, body text, links and more.

```tsx
import { Typography } from "@lifesg/react-design-system/typography";
```

<Canvas of={TypographyStories.TypographySet} />

<DocInfo>
**Which module to use?**<br /><br />

**Scenario 1: Text content**

If you are rendering text content, such as headings, paragraphs and links, use
this Typography component as it comes with additional capabilities to help you
lay out text.<br /><br />

**Scenario 2: Parent container**

If you are rendering a container that contains text children but does not need
to be a text element itself, apply the [typography design
tokens](/docs/foundations-typography-introduction--docs) directly.<br /><br />

**Scenario 3: HTML children**

If you are rendering a container that can contain arbitrary HTML markup, use the
[Markup component](/docs/general-markup--docs).

</DocInfo>

## Combining styles

### Inline text

You can nest text within text using the `inline` prop. The nested text is
rendered as a `span` element.

<Canvas of={TypographyStories.InlineText} />

### Inline link

You can include links within a set of text. Links are inline by default.

<Canvas of={TypographyStories.InlineLink} />

### Font weight

You can include different weights within a set of text using the `weight` and
`inline` props.

<Canvas of={TypographyStories.MixedFontWeights} />

## Paragraph specification

You can include paragraph spacing between text blocks by specifying the
`paragraph` prop.

<Canvas of={TypographyStories.Paragraphs} />

## External links

If a link leads to an external site, it is recommended to specify the `external`
prop, which displays an indicator.

<Canvas of={TypographyStories.ExternalLink} />

## Component API

<PropsTable />
Loading

0 comments on commit 1342b66

Please sign in to comment.