Skip to content

Commit 6cfe07a

Browse files
authored
ref(storybook): add primitives section (#96515)
Adds a new 'primitives' category to the storybook navigation tree to group fundamental layout and text components separately from other core components. - Add 'primitives' to StoryCategory type - Update category ordering to place primitives after foundations - Create isPrimitivesFile() to identify core/layout and core/text components - Modify inferFileCategory() to categorize primitives components correctly This improves component organization by creating a dedicated section for the most basic UI building blocks.
1 parent e77e179 commit 6cfe07a

File tree

11 files changed

+521
-207
lines changed

11 files changed

+521
-207
lines changed
Lines changed: 249 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,249 @@
1+
---
2+
title: Heading
3+
description: Heading components display semantic heading elements with appropriate typography and styling controls.
4+
source: 'sentry/components/core/text'
5+
resources:
6+
js: https://github.com/getsentry/sentry/blob/master/static/app/components/core/text/heading.tsx
7+
a11y:
8+
WCAG 1.3.1: https://www.w3.org/TR/WCAG22/#info-and-relationships
9+
WCAG 1.4.3: https://www.w3.org/TR/WCAG22/#contrast-minimum
10+
WCAG 1.4.4: https://www.w3.org/TR/WCAG22/#resize-text
11+
WCAG 1.4.6: https://www.w3.org/TR/WCAG22/#contrast-enhanced
12+
WCAG 1.4.8: https://www.w3.org/TR/WCAG22/#visual-presentation
13+
WCAG 1.4.12: https://www.w3.org/TR/WCAG22/#text-spacing
14+
WCAG 2.4.6: https://www.w3.org/TR/WCAG22/#headings-and-labels
15+
---
16+
17+
import {Heading} from 'sentry/components/core/text';
18+
import * as Storybook from 'sentry/stories';
19+
20+
import types from '!!type-loader!sentry/components/core/text/heading';
21+
22+
export {types};
23+
24+
The `<Heading>` component creates semantic heading elements with appropriate default sizes and styling. It ensures proper heading hierarchy while providing visual consistency.
25+
26+
```jsx
27+
<Heading as="h1">Page Title</Heading>
28+
<Heading as="h2">Section Title</Heading>
29+
<Heading as="h3">Subsection Title</Heading>
30+
```
31+
32+
### Semantic Heading Levels
33+
34+
Use the required `as` prop to specify the semantic heading level. Each level has a default size that follows standard typography hierarchy.
35+
36+
<Storybook.Demo>
37+
<Storybook.SideBySide vertical>
38+
<Heading as="h1">Heading 1</Heading>
39+
<Heading as="h2">Heading 2</Heading>
40+
<Heading as="h3">Heading 3</Heading>
41+
<Heading as="h4">Heading 4</Heading>
42+
<Heading as="h5">Heading 5</Heading>
43+
<Heading as="h6">Heading 6</Heading>
44+
</Storybook.SideBySide>
45+
</Storybook.Demo>
46+
```jsx
47+
<Heading as="h1">Heading 1</Heading>
48+
<Heading as="h2">Heading 2</Heading>
49+
<Heading as="h3">Heading 3</Heading>
50+
<Heading as="h4">Heading 4</Heading>
51+
<Heading as="h5">Heading 5</Heading>
52+
<Heading as="h6">Heading 6</Heading>
53+
```
54+
55+
### Custom Sizes
56+
57+
Override the default size with the `size` prop to decouple visual appearance from semantic meaning.
58+
59+
<Storybook.Demo>
60+
<Storybook.SideBySide vertical>
61+
<Heading as="h3" size="2xl">
62+
H3 with 2xl size
63+
</Heading>
64+
<Heading as="h1" size="md">
65+
H1 with medium size
66+
</Heading>
67+
<Heading as="h5" size="xl">
68+
H5 with extra large size
69+
</Heading>
70+
</Storybook.SideBySide>
71+
</Storybook.Demo>
72+
```jsx
73+
<Heading as="h3" size="2xl">
74+
H3 with 2xl size
75+
</Heading>
76+
<Heading as="h1" size="md">
77+
H1 with medium size
78+
</Heading>
79+
<Heading as="h5" size="xl">
80+
H5 with extra large size
81+
</Heading>
82+
```
83+
84+
### Variants
85+
86+
Headings support various color variants: `primary` (default), `muted`, `accent`, `success`, `warning`, `danger`, and `promotion`.
87+
88+
<Storybook.Demo>
89+
<Storybook.SideBySide vertical>
90+
<Heading as="h3" variant="primary">
91+
Primary heading (default)
92+
</Heading>
93+
<Heading as="h3" variant="muted">
94+
Muted heading
95+
</Heading>
96+
<Heading as="h3" variant="accent">
97+
Accent heading
98+
</Heading>
99+
<Heading as="h3" variant="success">
100+
Success heading
101+
</Heading>
102+
<Heading as="h3" variant="warning">
103+
Warning heading
104+
</Heading>
105+
<Heading as="h3" variant="danger">
106+
Danger heading
107+
</Heading>
108+
<Heading as="h3" variant="promotion">
109+
Promotion heading
110+
</Heading>
111+
</Storybook.SideBySide>
112+
</Storybook.Demo>
113+
```jsx
114+
<Heading as="h3" variant="primary">
115+
Primary heading (default)
116+
</Heading>
117+
<Heading as="h3" variant="muted">
118+
Muted heading
119+
</Heading>
120+
<Heading as="h3" variant="accent">
121+
Accent heading
122+
</Heading>
123+
<Heading as="h3" variant="success">
124+
Success heading
125+
</Heading>
126+
<Heading as="h3" variant="warning">
127+
Warning heading
128+
</Heading>
129+
<Heading as="h3" variant="danger">
130+
Danger heading
131+
</Heading>
132+
<Heading as="h3" variant="promotion">
133+
Promotion heading
134+
</Heading>
135+
```
136+
137+
### Typography Features
138+
139+
Headings support various typography options including italic, underline, strikethrough, and uppercase.
140+
141+
<Storybook.Demo>
142+
<Storybook.SideBySide vertical>
143+
<Heading as="h3" italic>
144+
Italic heading
145+
</Heading>
146+
<Heading as="h3" underline>
147+
Underlined heading
148+
</Heading>
149+
<Heading as="h3" strikethrough>
150+
Strikethrough heading
151+
</Heading>
152+
<Heading as="h3" uppercase>
153+
Uppercase heading
154+
</Heading>
155+
<Heading as="h3" italic underline>
156+
Italic underlined heading
157+
</Heading>
158+
</Storybook.SideBySide>
159+
</Storybook.Demo>
160+
```jsx
161+
<Heading as="h3" italic>
162+
Italic heading
163+
</Heading>
164+
<Heading as="h3" underline>
165+
Underlined heading
166+
</Heading>
167+
<Heading as="h3" strikethrough>
168+
Strikethrough heading
169+
</Heading>
170+
<Heading as="h3" uppercase>
171+
Uppercase heading
172+
</Heading>
173+
<Heading as="h3" italic underline>
174+
Italic underlined heading
175+
</Heading>
176+
```
177+
178+
### Text Alignment
179+
180+
Control text alignment with the `align` prop: `left` (default), `center`, `right`, or `justify`.
181+
182+
<Storybook.Demo>
183+
<Storybook.SideBySide vertical>
184+
<div style={{width: '300px', border: '1px solid #ccc', padding: '8px'}}>
185+
<Heading as="h4" align="left">
186+
Left aligned heading
187+
</Heading>
188+
</div>
189+
<div style={{width: '300px', border: '1px solid #ccc', padding: '8px'}}>
190+
<Heading as="h4" align="center">
191+
Center aligned heading
192+
</Heading>
193+
</div>
194+
<div style={{width: '300px', border: '1px solid #ccc', padding: '8px'}}>
195+
<Heading as="h4" align="right">
196+
Right aligned heading
197+
</Heading>
198+
</div>
199+
</Storybook.SideBySide>
200+
</Storybook.Demo>
201+
```jsx
202+
<Heading as="h4" align="left">
203+
Left aligned heading
204+
</Heading>
205+
<Heading as="h4" align="center">
206+
Center aligned heading
207+
</Heading>
208+
<Heading as="h4" align="right">
209+
Right aligned heading
210+
</Heading>
211+
```
212+
213+
### Ellipsis Overflow
214+
215+
Handle text overflow with the `ellipsis` prop to truncate long headings with an ellipsis.
216+
217+
<Storybook.Demo>
218+
<div style={{width: '200px', border: '1px solid #ccc', padding: '8px'}}>
219+
<Heading as="h4" ellipsis>
220+
This is a very long heading that will be truncated
221+
</Heading>
222+
</div>
223+
</Storybook.Demo>
224+
```jsx
225+
<div style={{width: '200px'}}>
226+
<Heading as="h4" ellipsis>
227+
This is a very long heading that will be truncated
228+
</Heading>
229+
</div>
230+
```
231+
232+
### Monospace
233+
234+
Use `monospace` for fixed-width headings, useful for code-related content.
235+
236+
<Storybook.Demo>
237+
<Storybook.SideBySide vertical>
238+
<Heading as="h4">API Response Format</Heading>
239+
<Heading as="h4" monospace>
240+
API Response Format
241+
</Heading>
242+
</Storybook.SideBySide>
243+
</Storybook.Demo>
244+
```jsx
245+
<Heading as="h4">API Response Format</Heading>
246+
<Heading as="h4" monospace>
247+
API Response Format
248+
</Heading>
249+
```
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import {createRef} from 'react';
2+
3+
import {render, screen} from 'sentry-test/reactTestingLibrary';
4+
5+
import {Heading} from './';
6+
7+
describe('Heading', () => {
8+
it('renders with correct HTML element', () => {
9+
render(
10+
<Heading as="h6" align="center">
11+
Heading 6
12+
</Heading>
13+
);
14+
expect(screen.getByText('Heading 6').tagName).toBe('H6');
15+
});
16+
17+
it('does not bleed props to the DOM element', () => {
18+
render(
19+
<Heading as="h6" align="center">
20+
Heading 6
21+
</Heading>
22+
);
23+
expect(screen.getByText('Heading 6')).not.toHaveAttribute('align');
24+
});
25+
26+
it('forwards data-test-id', () => {
27+
render(
28+
<Heading as="h6" data-test-id="test-id">
29+
Heading 6
30+
</Heading>
31+
);
32+
expect(screen.getByText('Heading 6')).toHaveAttribute('data-test-id', 'test-id');
33+
});
34+
it('allows passing native HTML attributes', () => {
35+
render(
36+
<Heading as="h6" style={{color: 'red'}}>
37+
Heading 6
38+
</Heading>
39+
);
40+
expect(screen.getByText('Heading 6')).toHaveStyle({color: 'red'});
41+
});
42+
it('assings ref', () => {
43+
const ref = createRef<HTMLHeadingElement>();
44+
render(
45+
<Heading as="h6" ref={ref}>
46+
Heading 6
47+
</Heading>
48+
);
49+
expect(ref.current?.tagName).toBe('H6');
50+
});
51+
});
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import isPropValid from '@emotion/is-prop-valid';
2+
import styled from '@emotion/styled';
3+
4+
import {getFontSize, getLineHeight, getTextDecoration} from './styles';
5+
import {
6+
type BaseTextProps,
7+
type ExclusiveTextEllipsisProps,
8+
type TextProps,
9+
} from './text';
10+
11+
type BaseHeadingProps = Omit<BaseTextProps, 'bold'>;
12+
13+
export type HeadingProps = BaseHeadingProps & {
14+
/**
15+
* The HTML element to render the title as.
16+
* @required
17+
*/
18+
as: 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6';
19+
ref?: React.Ref<HTMLHeadingElement | null> | undefined;
20+
} & React.HTMLAttributes<HTMLHeadingElement> &
21+
ExclusiveTextEllipsisProps;
22+
23+
export const Heading = styled(
24+
(props: HeadingProps) => {
25+
const {children, as, ...rest} = props;
26+
const HeadingComponent = as;
27+
28+
return <HeadingComponent {...rest}>{children}</HeadingComponent>;
29+
},
30+
{
31+
shouldForwardProp: p => isPropValid(p),
32+
}
33+
)`
34+
font-size: ${p => getFontSize(p.size ?? getDefaultHeadingFontSize(p.as), p.theme)};
35+
font-style: ${p => (p.italic ? 'italic' : undefined)};
36+
37+
line-height: ${p => getLineHeight(p.density)};
38+
text-decoration: ${p => getTextDecoration(p)};
39+
40+
color: ${p => p.theme.tokens.content[p.variant ?? 'primary']};
41+
text-align: ${p => p.align ?? 'left'};
42+
43+
overflow: ${p => (p.ellipsis ? 'hidden' : undefined)};
44+
text-overflow: ${p => (p.ellipsis ? 'ellipsis' : undefined)};
45+
white-space: ${p => (p.wrap ? p.wrap : p.ellipsis ? 'nowrap' : undefined)};
46+
47+
font-family: ${p => (p.monospace ? p.theme.text.familyMono : p.theme.text.family)};
48+
font-weight: ${p => p.theme.fontWeight.bold};
49+
font-variant-numeric: ${p =>
50+
[
51+
p.tabular ? 'tabular-nums' : undefined,
52+
p.fraction ? 'diagonal-fractions' : undefined,
53+
]
54+
.filter(Boolean)
55+
.join(' ')};
56+
text-transform: ${p => (p.uppercase ? 'uppercase' : undefined)};
57+
58+
text-box-edge: text text;
59+
text-box-trim: trim-both;
60+
61+
/**
62+
* Reset any margin or padding that might be set by the global CSS styles.
63+
*/
64+
margin: 0;
65+
padding: 0;
66+
`;
67+
68+
function getDefaultHeadingFontSize(as: HeadingProps['as']): TextProps<any>['size'] {
69+
switch (as) {
70+
case 'h1':
71+
return '2xl';
72+
case 'h2':
73+
return 'xl';
74+
case 'h3':
75+
return 'lg';
76+
case 'h4':
77+
return 'md';
78+
case 'h5':
79+
return 'sm';
80+
case 'h6':
81+
return 'xs';
82+
default:
83+
return '2xl';
84+
}
85+
}
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
1-
export {Text, Heading} from './text';
1+
export {Text} from './text';
2+
export {Heading} from './heading';

0 commit comments

Comments
 (0)