Skip to content

Commit c7aa09a

Browse files
Zuzzepd-sa-github-launchpad-uiSusanna Nevalainen
authored
feat(components): Typography token styles added to Heading, Text, Label, Code components (#1725)
* feat(components): extend Heading and Text with variant, align, and maxLines props * feat(components): changeset added * feat(components): heading variant default to heading3Regular to align react-aria default level h3 * feat(components): css truncate cleanup * feat(components): import from @storybook/react-vite instead of @storybook/react * feat(components): text and heading stories interlinking * feat(components): default variants set to empty to inherit styles, removed align prop * feat(components): reverted storybook main prop filter * feat(components): label component story and variants added * feat(components): label and code props aligned based on feedback * feat(components): text component updated with size and bold props * feat(components): heading component updated with bold and size props * feat(components): label component cleanup * feat(components): label component ref documentation update * feat(components): remove unnecessary important from truncate class * feat(components): default text component colors to inherit to avoid breaking changes * feat(components): button size inherited to button text, label size added * feat(components): label css polishments * feat(components): snapshot test fix for horizontal form --------- Co-authored-by: pd-sa-github-launchpad-ui <[email protected]> Co-authored-by: Susanna Nevalainen <[email protected]>
1 parent ac02abe commit c7aa09a

20 files changed

+745
-29
lines changed

.changeset/sharp-states-slide.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@launchpad-ui/components": minor
3+
---
4+
5+
Heading and Text props extended

.storybook/main.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ const config: StorybookConfig = {
4545
prop.parent.fileName,
4646
)
4747
: true,
48+
shouldExtractValuesFromUnion: true,
4849
},
4950
},
5051
};

packages/components/src/Button.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,11 @@ const Button = ({ ref, ...props }: ButtonProps) => {
7272
{isPending && (
7373
<ProgressBar isIndeterminate aria-label="loading" className={styles.progress} />
7474
)}
75-
{typeof children === 'string' ? <Text>{children}</Text> : children}
75+
{typeof children === 'string' ? (
76+
<Text size={size as 'small' | 'medium' | 'large'}>{children}</Text>
77+
) : (
78+
children
79+
)}
7680
</Provider>
7781
))}
7882
</AriaButton>

packages/components/src/Code.tsx

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { cva, cx } from 'class-variance-authority';
2+
import { forwardRef } from 'react';
3+
4+
import styles from './styles/Code.module.css';
5+
6+
const codeStyles = cva(styles.default, {
7+
variants: {
8+
size: {
9+
small: styles.small,
10+
medium: styles.medium,
11+
},
12+
},
13+
defaultVariants: {
14+
size: 'small',
15+
},
16+
});
17+
18+
export interface CodeProps extends Omit<React.HTMLAttributes<HTMLElement>, 'className'> {
19+
children: React.ReactNode;
20+
/** Text size */
21+
size?: 'small' | 'medium';
22+
/** Optional CSS class name */
23+
className?: string;
24+
}
25+
26+
/**
27+
* A Code component for displaying inline code snippets.
28+
*
29+
* For body text, use `Text`. For headings, use `Heading`. For labels, use `Label`.
30+
*/
31+
const Code = forwardRef<HTMLElement, CodeProps>(({ children, size, className, ...props }, ref) => {
32+
return (
33+
<code {...props} ref={ref} className={cx(codeStyles({ size }), styles.code, className)}>
34+
{children}
35+
</code>
36+
);
37+
});
38+
39+
Code.displayName = 'Code';
40+
41+
export { Code };

packages/components/src/Heading.tsx

Lines changed: 72 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,92 @@
11
import type { Ref } from 'react';
22
import type { HeadingProps as AriaHeadingProps, ContextValue } from 'react-aria-components';
33

4-
import { cva } from 'class-variance-authority';
4+
import { cva, cx } from 'class-variance-authority';
55
import { createContext } from 'react';
66
import { Heading as AriaHeading } from 'react-aria-components';
77

88
import styles from './styles/Heading.module.css';
99
import { useLPContextProps } from './utils';
1010

11-
const headingStyles = cva(styles.heading);
11+
const headingStyles = cva(styles.heading, {
12+
variants: {
13+
size: {
14+
small: styles.small,
15+
medium: styles.medium,
16+
large: styles.large,
17+
},
18+
bold: {
19+
true: styles.bold,
20+
},
21+
},
22+
defaultVariants: {
23+
size: 'medium',
24+
bold: true,
25+
},
26+
});
1227

13-
interface HeadingProps extends AriaHeadingProps {
28+
interface HeadingProps extends Omit<AriaHeadingProps, 'className' | 'level'> {
1429
ref?: Ref<HTMLHeadingElement>;
30+
/** Heading size */
31+
size?: 'small' | 'medium' | 'large';
32+
/** Whether to use bold font weight */
33+
bold?: boolean;
34+
/** Maximum number of lines to display. Overflowing text will be truncated with an ellipsis. */
35+
maxLines?: number;
36+
/** Optional CSS class name */
37+
className?: AriaHeadingProps['className'];
38+
/** Optional HTML heading level. Defaults based on size (large=1, medium=2, small=3). */
39+
level?: AriaHeadingProps['level'];
1540
}
1641

1742
const HeadingContext = createContext<ContextValue<HeadingProps, HTMLHeadingElement>>(null);
1843

19-
const Heading = ({ ref, ...props }: HeadingProps) => {
44+
const getDefaultLevel = (size: 'small' | 'medium' | 'large'): AriaHeadingProps['level'] => {
45+
switch (size) {
46+
case 'large':
47+
return 1;
48+
case 'medium':
49+
return 2;
50+
case 'small':
51+
return 3;
52+
}
53+
};
54+
55+
/**
56+
* A generic Heading component for headings.
57+
*
58+
* For other text, use `Text`, `Label`, or `Code`.
59+
*
60+
* Built on top of [React Aria `Heading` component](https://react-spectrum.adobe.com/react-spectrum/Heading.html#heading).
61+
*/
62+
const Heading = ({
63+
ref,
64+
size = 'medium',
65+
bold = true,
66+
maxLines,
67+
className,
68+
style,
69+
level,
70+
...props
71+
}: HeadingProps) => {
2072
[props, ref] = useLPContextProps(props, ref, HeadingContext);
21-
const { className } = props;
2273

23-
return <AriaHeading {...props} ref={ref} className={headingStyles({ className })} />;
74+
return (
75+
<AriaHeading
76+
{...props}
77+
ref={ref}
78+
level={level || getDefaultLevel(size)}
79+
className={cx(headingStyles({ size, bold }), maxLines && styles.truncate, className)}
80+
style={{
81+
...style,
82+
...(maxLines && {
83+
WebkitLineClamp: maxLines,
84+
}),
85+
}}
86+
>
87+
{props.children}
88+
</AriaHeading>
89+
);
2490
};
2591

2692
export { Heading, HeadingContext, headingStyles };

packages/components/src/Label.tsx

Lines changed: 45 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,62 @@
1-
import type { Ref } from 'react';
21
import type { LabelProps as AriaLabelProps, ContextValue } from 'react-aria-components';
32

43
import { cva, cx } from 'class-variance-authority';
5-
import { createContext } from 'react';
4+
import { createContext, type Ref } from 'react';
65
import { Label as AriaLabel } from 'react-aria-components';
76

87
import styles from './styles/Label.module.css';
98
import { useLPContextProps } from './utils';
109

11-
const labelStyles = cva(styles.label);
12-
13-
interface LabelProps extends AriaLabelProps {
10+
const labelStyles = cva(styles.label, {
11+
variants: {
12+
size: {
13+
small: styles.small,
14+
medium: styles.medium,
15+
},
16+
},
17+
defaultVariants: {
18+
size: 'medium',
19+
},
20+
});
21+
22+
interface LabelProps extends Omit<AriaLabelProps, 'className'> {
1423
ref?: Ref<HTMLLabelElement>;
24+
size?: 'small' | 'medium';
25+
/** Maximum number of lines to display. Overflowing text will be truncated with an ellipsis. */
26+
maxLines?: number;
27+
/** Optional CSS class name */
28+
className?: AriaLabelProps['className'];
1529
}
1630

1731
const LabelContext = createContext<ContextValue<LabelProps, HTMLLabelElement>>(null);
1832

19-
const Label = ({ ref, ...props }: LabelProps) => {
20-
[props, ref] = useLPContextProps(props, ref, LabelContext);
21-
const { className } = props;
22-
23-
return <AriaLabel {...props} ref={ref} className={cx(labelStyles({ className }))} />;
33+
/**
34+
* A generic Label component for form labels.
35+
*
36+
* For body text, use `Text`. For headings, use `Heading`
37+
*
38+
* Built on top of [React Aria `Label` component](https://react-spectrum.adobe.com/react-spectrum/Label.html#label).
39+
*/
40+
const Label = ({ ref, maxLines, style, size, ...props }: LabelProps) => {
41+
const [contextProps, contextRef] = useLPContextProps(props, ref, LabelContext);
42+
const { className } = contextProps as any;
43+
44+
return (
45+
<AriaLabel
46+
{...contextProps}
47+
ref={contextRef}
48+
className={cx(labelStyles({ size }), maxLines && styles.truncate, className, props.className)}
49+
style={{
50+
...style,
51+
...(maxLines && {
52+
WebkitLineClamp: maxLines,
53+
}),
54+
}}
55+
/>
56+
);
2457
};
2558

59+
Label.displayName = 'Label';
60+
2661
export { Label, LabelContext, labelStyles };
2762
export type { LabelProps };

packages/components/src/Text.tsx

Lines changed: 68 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,88 @@
11
import type { Ref } from 'react';
22
import type { TextProps as AriaTextProps, ContextValue } from 'react-aria-components';
33

4-
import { cva } from 'class-variance-authority';
4+
import { cva, cx } from 'class-variance-authority';
55
import { createContext } from 'react';
66
import { Text as AriaText } from 'react-aria-components';
77

88
import styles from './styles/Text.module.css';
99
import { useLPContextProps } from './utils';
1010

11-
const textStyles = cva(styles.text);
11+
const textStyles = cva(styles.text, {
12+
variants: {
13+
size: {
14+
small: styles.small,
15+
medium: styles.medium,
16+
large: styles.large,
17+
},
18+
bold: {
19+
true: styles.bold,
20+
},
21+
},
22+
defaultVariants: {
23+
size: 'medium',
24+
bold: false,
25+
},
26+
});
1227

13-
interface TextProps extends AriaTextProps {
28+
interface TextProps extends Omit<AriaTextProps, 'className' | 'elementType'> {
1429
ref?: Ref<HTMLElement>;
30+
/** Text size */
31+
size?: 'small' | 'medium' | 'large';
32+
/** Whether to use bold font weight */
33+
bold?: boolean;
34+
/** Maximum number of lines to display. Overflowing text will be truncated with an ellipsis. */
35+
maxLines?: number;
36+
/** Optional HTML element type such as `span`, `label`, `small`, `code`, `p`, etc. Defaults to `p` for medium/large, `small` for small size. */
37+
elementType?: AriaTextProps['elementType'];
38+
/** Optional CSS class name */
39+
className?: AriaTextProps['className'];
1540
}
1641

1742
const TextContext = createContext<ContextValue<TextProps, HTMLElement>>(null);
1843

19-
const Text = ({ ref, ...props }: TextProps) => {
44+
const getDefaultElementType = (size: 'small' | 'medium' | 'large'): string => {
45+
if (size === 'small') {
46+
return 'small';
47+
}
48+
return 'span';
49+
};
50+
51+
/**
52+
* A generic Text component for body text.
53+
*
54+
* For headings, use [Heading](/docs/components-content-heading--docs)
55+
*
56+
* Built on top of [React Aria `Text` component](https://react-spectrum.adobe.com/react-spectrum/Text.html#text).
57+
*/
58+
const Text = ({
59+
ref,
60+
size = 'medium',
61+
bold = false,
62+
maxLines,
63+
elementType,
64+
className,
65+
style,
66+
...props
67+
}: TextProps) => {
2068
[props, ref] = useLPContextProps(props, ref, TextContext);
21-
const { className } = props;
2269

23-
return <AriaText {...props} ref={ref} className={textStyles({ className })} />;
70+
return (
71+
<AriaText
72+
{...props}
73+
ref={ref}
74+
elementType={elementType || getDefaultElementType(size)}
75+
className={cx(textStyles({ size, bold }), maxLines && styles.truncate, className)}
76+
style={{
77+
...style,
78+
...(maxLines && {
79+
WebkitLineClamp: maxLines,
80+
}),
81+
}}
82+
>
83+
{props.children}
84+
</AriaText>
85+
);
2486
};
2587

2688
export { Text, TextContext, textStyles };

packages/components/src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ export type {
1818
} from './Calendar';
1919
export type { CheckboxProps } from './Checkbox';
2020
export type { CheckboxGroupProps } from './CheckboxGroup';
21+
export type { CodeProps } from './Code';
2122
export type { ComboBoxProps } from './ComboBox';
2223
export type { DateFieldProps, DateInputProps, DateSegmentProps, TimeFieldProps } from './DateField';
2324
export type { DatePickerProps, DateRangePickerProps } from './DatePicker';
@@ -118,6 +119,7 @@ export {
118119
checkboxStyles,
119120
} from './Checkbox';
120121
export { CheckboxGroup, CheckboxGroupContext, checkboxGroupStyles } from './CheckboxGroup';
122+
export { Code } from './Code';
121123
export { Collection } from './Collection';
122124
export { ComboBox, ComboBoxClearButton, ComboBoxContext, comboBoxStyles } from './ComboBox';
123125
export {
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
.code {
2+
background: transparent;
3+
font-weight: var(--lp-font-weight-regular);
4+
border-radius: var(--lp-border-radius-regular);
5+
color: var(--lp-color-text-code-string);
6+
width: fit-content;
7+
background-color: var(--lp-color-bg-ui-tertiary);
8+
padding-top: var(--lp-spacing-100);
9+
padding-bottom: var(--lp-spacing-100);
10+
padding-left: var(--lp-spacing-200);
11+
padding-right: var(--lp-spacing-200);
12+
border: none;
13+
outline: none;
14+
}
15+
16+
.small {
17+
font-size: var(--lp-font-size-100);
18+
}
19+
20+
.medium {
21+
font-size: var(--lp-font-size-200);
22+
}

0 commit comments

Comments
 (0)