Skip to content

Commit c083863

Browse files
authored
Updated semantics for the Statistic component (#991)
1 parent bb24a54 commit c083863

13 files changed

+104
-49
lines changed

.changeset/cold-days-doubt.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
---
2+
'@primer/react-brand': minor
3+
---
4+
5+
Updated the underlying HTML elements in the `Statistic` component for improved accessibility. Now a paragraph by default, where it was previously a heading. It can optionally also be set as a `<span>` using the `as` prop.
6+
7+
⚠️ Breaking changes:
8+
9+
- `stretch` prop in `Statistic.Heading` has been removed.
10+
- `as` prop now accepts `p` and `span` only
11+
- `size` prop now accepts `1000`, `900`, `800`, `700`, `600`, `500`, `400`, `300`, `200`

.changeset/early-buses-peel.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@primer/react-brand': patch
3+
---
4+
5+
Additional `size` options available in the `Text` component: `800`, `900`, `1000`.

packages/react/src/Statistic/Statistic.features.stories.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,8 @@ export const Sizes = args => (
4747
export const CustomHeadingSize = () => (
4848
<Stack direction="vertical" gap="spacious" padding="none">
4949
<Statistic size="small">
50-
<Statistic.Heading size="1">Smallest size</Statistic.Heading>
51-
<Statistic.Description>w/ size 1 heading override</Statistic.Description>
50+
<Statistic.Heading size="100">Smallest size</Statistic.Heading>
51+
<Statistic.Description>w/ size 100 text override</Statistic.Description>
5252
</Statistic>
5353
</Stack>
5454
)

packages/react/src/Statistic/Statistic.test.tsx

Lines changed: 43 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -9,46 +9,74 @@ import '../test-utils/mocks/match-media-mock'
99

1010
expect.extend(toHaveNoViolations)
1111

12-
describe('EyebrowBanner', () => {
12+
describe('Statistic', () => {
1313
const mockHeading = '$100M+'
1414
const mockDescription = 'developers'
1515

16-
const mockReadAloudText = `${mockHeading} ${mockDescription}`
16+
const expectedReadAloudText = `${mockHeading} ${mockDescription}`
1717

1818
afterEach(() => {
1919
cleanup()
2020
jest.clearAllMocks()
2121
})
2222

2323
it('renders the default component', async () => {
24-
const {getByRole, container} = render(
24+
const {getByText, container} = render(
2525
<Statistic>
2626
<Statistic.Heading>{mockHeading}</Statistic.Heading>
2727
<Statistic.Description>{mockDescription}</Statistic.Description>
2828
</Statistic>,
2929
)
3030

31-
expect(getByRole('heading', {name: mockReadAloudText})).toBeInTheDocument()
31+
expect(getByText(mockHeading)).toBeInTheDocument()
32+
expect(getByText(mockDescription)).toBeInTheDocument()
3233

3334
const results = await axe(container)
3435

3536
expect(results).toHaveNoViolations()
3637
})
3738

38-
it('renders the default heading level and size correctly', async () => {
39-
const expectedSize = 'display'
40-
const expectedLevel = 'H3'
39+
it('renders the combined heading and description text', async () => {
40+
const {getByTestId} = render(
41+
<Statistic>
42+
<Statistic.Heading>{mockHeading}</Statistic.Heading>
43+
<Statistic.Description>{mockDescription}</Statistic.Description>
44+
</Statistic>,
45+
)
4146

42-
const {getByText, getByRole} = render(
47+
const el = getByTestId(Statistic.testIds.root)
48+
49+
expect(el.textContent).toBe(expectedReadAloudText)
50+
})
51+
52+
it('renders the Statistic.Heading with a paragraph element by default', async () => {
53+
const expectedTagName = 'P'
54+
const expectedSize = '1000'
55+
56+
const {getByText} = render(
4357
<Statistic>
4458
<Statistic.Heading>{mockHeading}</Statistic.Heading>
4559
<Statistic.Description>{mockDescription}</Statistic.Description>
4660
</Statistic>,
4761
)
4862

49-
expect(getByRole('heading', {name: mockReadAloudText}).tagName).toBe(expectedLevel)
50-
expect(getByRole('heading', {name: mockReadAloudText}).classList).toContain(`Heading--${expectedSize}`)
51-
expect(getByText(mockHeading).tagName).toBe(expectedLevel)
63+
expect(getByText(mockHeading).tagName).toBe(expectedTagName)
64+
expect(getByText(mockHeading).classList).toContain(`Text--${expectedSize}`)
65+
})
66+
67+
it('renders the Statistic.Heading with a span element when as="span" is passed', async () => {
68+
const expectedTagName = 'SPAN'
69+
const expectedSize = '1000'
70+
71+
const {getByText} = render(
72+
<Statistic>
73+
<Statistic.Heading as="span">{mockHeading}</Statistic.Heading>
74+
<Statistic.Description>{mockDescription}</Statistic.Description>
75+
</Statistic>,
76+
)
77+
78+
expect(getByText(mockHeading).tagName).toBe(expectedTagName)
79+
expect(getByText(mockHeading).classList).toContain(`Text--${expectedSize}`)
5280
})
5381

5482
it('renders the Statistic with custom padding', async () => {
@@ -110,16 +138,16 @@ describe('EyebrowBanner', () => {
110138
})
111139

112140
it('renders the Statistic with custom heading size', async () => {
113-
const {getByRole} = render(
141+
const {getByText} = render(
114142
<Statistic>
115-
<Statistic.Heading size="2">{mockHeading}</Statistic.Heading>
143+
<Statistic.Heading size="600">{mockHeading}</Statistic.Heading>
116144
<Statistic.Description>{mockDescription}</Statistic.Description>
117145
</Statistic>,
118146
)
119147

120-
const heading = getByRole('heading', {name: mockReadAloudText})
148+
const heading = getByText(mockHeading)
121149

122-
expect(heading).toHaveClass('Heading--2')
150+
expect(heading).toHaveClass('Text--600')
123151
})
124152

125153
it('renders the Statistic with custom description variant', async () => {

packages/react/src/Statistic/Statistic.tsx

Lines changed: 13 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import React, {PropsWithChildren, forwardRef, useMemo} from 'react'
22
import clsx from 'clsx'
33

4-
import {Text, Heading, HeadingProps, TextProps, useAnimation} from '../'
4+
import {Text, TextProps, useAnimation} from '../'
55

66
import type {BaseProps} from '../component-helpers'
77

@@ -65,11 +65,11 @@ const testIds = {
6565
}
6666

6767
const _HeadingSizeMap: {
68-
[key in StatisticSize]: HeadingProps['size']
68+
[key in StatisticSize]: TextProps['size']
6969
} = {
70-
small: '4',
71-
medium: '2',
72-
large: 'display',
70+
small: '600',
71+
medium: '800',
72+
large: '1000',
7373
}
7474

7575
const classBuilder = (property: string, value?: StatisticSpacingValues | ResponsiveMap<StatisticSpacingValues>) => {
@@ -129,10 +129,10 @@ const _Statistic = forwardRef<HTMLDivElement, PropsWithChildren<StatisticProps>>
129129
*/
130130
if (DescriptionChild) {
131131
const updatedHeadingChild = React.isValidElement(HeadingChild)
132-
? React.cloneElement(HeadingChild as React.ReactElement<HeadingProps>, {
132+
? React.cloneElement(HeadingChild as React.ReactElement<TextProps>, {
133133
children: (
134134
<>
135-
{HeadingChild.props.children}
135+
{`${HeadingChild.props.children} `}
136136
{DescriptionChild}
137137
</>
138138
),
@@ -153,7 +153,7 @@ const _Statistic = forwardRef<HTMLDivElement, PropsWithChildren<StatisticProps>>
153153
{LeadingComponent && <LeadingComponent />}
154154

155155
{React.isValidElement(HeadingChild) &&
156-
React.cloneElement(HeadingChild as React.ReactElement<HeadingProps>, {
156+
React.cloneElement(HeadingChild as React.ReactElement<TextProps>, {
157157
size: HeadingChild.props.size || _HeadingSizeMap[size],
158158
})}
159159

@@ -163,35 +163,22 @@ const _Statistic = forwardRef<HTMLDivElement, PropsWithChildren<StatisticProps>>
163163
},
164164
)
165165

166-
type StatisticHeadingProps = HeadingProps
166+
type StatisticHeadingProps = TextProps
167167

168-
const StatisticHeading = forwardRef<HTMLHeadingElement, StatisticHeadingProps>(
169-
(
170-
{
171-
as = 'h3',
172-
className,
173-
children,
174-
font = 'hubot-sans',
175-
weight = 'semibold',
176-
stretch = 'condensed',
177-
size = 'display',
178-
...rest
179-
},
180-
ref,
181-
) => {
168+
const StatisticHeading = forwardRef<HTMLParagraphElement, StatisticHeadingProps>(
169+
({as = 'p', className, children, font = 'hubot-sans', weight = 'semibold', size = '1000', ...rest}, ref) => {
182170
return (
183-
<Heading
171+
<Text
184172
as={as}
185173
ref={ref}
186174
className={clsx(styles[`Statistic__heading`], className)}
187175
font={font}
188176
weight={weight}
189-
stretch={stretch}
190177
size={size}
191178
{...rest}
192179
>
193180
{children}
194-
</Heading>
181+
</Text>
195182
)
196183
},
197184
)

packages/react/src/Text/Text.module.css

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,27 @@
2727
font-family: var(--brand-fontStack-monospace);
2828
}
2929

30+
.Text--1000 {
31+
font-weight: var(--brand-text-weight-1000);
32+
font-size: var(--brand-text-size-1000);
33+
line-height: var(--brand-text-lineHeight-1000);
34+
letter-spacing: var(--brand-text-letterSpacing-1000);
35+
}
36+
37+
.Text--900 {
38+
font-weight: var(--brand-text-weight-900);
39+
font-size: var(--brand-text-size-900);
40+
line-height: var(--brand-text-lineHeight-900);
41+
letter-spacing: var(--brand-text-letterSpacing-900);
42+
}
43+
44+
.Text--800 {
45+
font-weight: var(--brand-text-weight-800);
46+
font-size: var(--brand-text-size-800);
47+
line-height: var(--brand-text-lineHeight-800);
48+
letter-spacing: var(--brand-text-letterSpacing-800);
49+
}
50+
3051
.Text--700 {
3152
font-weight: var(--brand-text-weight-700);
3253
font-size: var(--brand-text-size-700);

packages/react/src/Text/Text.module.css.d.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
declare const styles: {
22
readonly "Text": string;
33
readonly "Text--100": string;
4+
readonly "Text--1000": string;
45
readonly "Text--200": string;
56
readonly "Text--300": string;
67
readonly "Text--400": string;
78
readonly "Text--500": string;
89
readonly "Text--600": string;
910
readonly "Text--700": string;
11+
readonly "Text--800": string;
12+
readonly "Text--900": string;
1013
readonly "Text--antialiased": string;
1114
readonly "Text--default": string;
1215
readonly "Text--muted": string;

packages/react/src/Text/Text.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import {useAnimation} from '../'
44
import styles from './Text.module.css'
55
import {BaseProps} from '../component-helpers'
66

7-
export const TextSizes = ['700', '600', '500', '400', '300', '200', '100'] as const
7+
export const TextSizes = ['1000', '900', '800', '700', '600', '500', '400', '300', '200', '100'] as const
88
export const TextTags = ['p', 'span', 'div', 'strong', 'em'] as const
99
export const TextVariants = ['default', 'muted'] as const
1010
export const TextWeights = [
@@ -20,7 +20,7 @@ export const TextWeights = [
2020
export const TextFontVariants = ['mona-sans', 'hubot-sans', 'monospace'] as const
2121

2222
export const defaultTextTag = TextTags[1]
23-
export const defaultTextSize = TextSizes[5]
23+
export const defaultTextSize = TextSizes[8]
2424
export const defaultTextVariant = TextVariants[0]
2525
export const defaultFontVariant = TextFontVariants[0]
2626

packages/react/src/recipes/SolutionTemplates/CategoryPage/CategoryPage.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -274,30 +274,30 @@ export function CategoryPage({
274274
<Grid.Column span={{medium: 6, large: 3}}>
275275
<Box paddingBlockEnd={{narrow: 24, regular: 'none'}}>
276276
<Statistic size="medium" animate="fade-in">
277-
<Statistic.Heading size="1">$2M+</Statistic.Heading>
277+
<Statistic.Heading size="900">$2M+</Statistic.Heading>
278278
<Statistic.Description>Given back to our maintainers</Statistic.Description>
279279
</Statistic>
280280
</Box>
281281
</Grid.Column>
282282
<Grid.Column span={{medium: 6, large: 3}}>
283283
<Box paddingBlockEnd={{narrow: 24, regular: 'none'}}>
284284
<Statistic size="medium" animate="fade-in">
285-
<Statistic.Heading size="1">~25%</Statistic.Heading>
285+
<Statistic.Heading size="900">~25%</Statistic.Heading>
286286
<Statistic.Description>increase in developer speed with GitHub Copilot</Statistic.Description>
287287
</Statistic>
288288
</Box>
289289
</Grid.Column>
290290
<Grid.Column span={{medium: 6, large: 3}}>
291291
<Box paddingBlockEnd={{narrow: 24, regular: 'none'}}>
292292
<Statistic size="medium" animate="fade-in">
293-
<Statistic.Heading size="1">1min</Statistic.Heading>
293+
<Statistic.Heading size="900">1min</Statistic.Heading>
294294
<Statistic.Description>set-up time for largest repo with Codespaces</Statistic.Description>
295295
</Statistic>
296296
</Box>
297297
</Grid.Column>
298298
<Grid.Column span={{medium: 6, large: 3}}>
299299
<Statistic size="medium" animate="fade-in">
300-
<Statistic.Heading size="1">3.5K+</Statistic.Heading>
300+
<Statistic.Heading size="900">3.5K+</Statistic.Heading>
301301
<Statistic.Description>Companies actively sponsoring</Statistic.Description>
302302
</Statistic>
303303
</Grid.Column>

0 commit comments

Comments
 (0)