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

Set up V3 Typography component #570

Merged
merged 10 commits into from
Oct 1, 2024
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
Loading