Skip to content

Commit 5055b91

Browse files
authored
1572 accessibility request primer react heading enforce what as can be set to with a runtime validation rule (#2768)
* fix(Heading): as prop fix, testing type and logging * chore: add another changeset * Delete small-bikes-carry.md Not needed * chore(Heading): resolved type check error from test
1 parent 857d34b commit 5055b91

File tree

4 files changed

+58
-1
lines changed

4 files changed

+58
-1
lines changed

.changeset/fifty-dolls-speak.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@primer/react': minor
3+
---
4+
5+
Confine Heading as prop to header element types

src/Heading.tsx

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,52 @@
1+
import React, {forwardRef, useEffect} from 'react'
12
import styled from 'styled-components'
23
import {get} from './constants'
4+
import {useRefObjectAsForwardedRef} from './hooks'
35
import sx, {SxProp} from './sx'
46
import {ComponentProps} from './utils/types'
7+
import {ForwardRefComponent as PolymorphicForwardRefComponent} from './utils/polymorphic'
58

6-
const Heading = styled.h2<SxProp>`
9+
type StyledHeadingProps = {
10+
as?: 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6'
11+
} & SxProp
12+
13+
const StyledHeading = styled.h2<StyledHeadingProps>`
714
font-weight: ${get('fontWeights.bold')};
815
font-size: ${get('fontSizes.5')};
916
margin: 0;
1017
${sx};
1118
`
19+
const Heading = forwardRef(({as: Component = 'h2', ...props}, forwardedRef) => {
20+
const innerRef = React.useRef<HTMLHeadingElement>(null)
21+
useRefObjectAsForwardedRef(forwardedRef, innerRef)
22+
23+
if (__DEV__) {
24+
/**
25+
* The Linter yells because it thinks this conditionally calls an effect,
26+
* but since this is a compile-time flag and not a runtime conditional
27+
* this is safe, and ensures the entire effect is kept out of prod builds
28+
* shaving precious bytes from the output, and avoiding mounting a noop effect
29+
*/
30+
// eslint-disable-next-line react-hooks/rules-of-hooks
31+
useEffect(() => {
32+
if (innerRef.current && !(innerRef.current instanceof HTMLHeadingElement)) {
33+
// eslint-disable-next-line no-console
34+
console.warn('This Heading component should be an instanceof of h1-h6')
35+
}
36+
}, [innerRef])
37+
}
38+
39+
return (
40+
<StyledHeading
41+
as={Component}
42+
{...props}
43+
// @ts-ignore shh
44+
ref={innerRef}
45+
/>
46+
)
47+
}) as PolymorphicForwardRefComponent<'h2', StyledHeadingProps>
48+
49+
Heading.displayName = 'Heading'
1250

1351
export type HeadingProps = ComponentProps<typeof Heading>
1452
export default Heading

src/__tests__/Heading.test.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,15 @@ describe('Heading', () => {
122122
).toHaveStyleRule('font-size', `${fontSize}`)
123123
}
124124
})
125+
it('logs a warning when trying to render invalid "as" prop', () => {
126+
const consoleSpy = jest.spyOn(global.console, 'warn').mockImplementation()
127+
128+
// @ts-expect-error as prop should not be accepted
129+
HTMLRender(<Heading as="i" />)
130+
expect(consoleSpy).toHaveBeenCalled()
131+
132+
consoleSpy.mockRestore()
133+
})
125134

126135
it('respects the "fontStyle" prop', () => {
127136
expect(

src/__tests__/Heading.types.test.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,8 @@ export function shouldNotAcceptSystemProps() {
99
// @ts-expect-error system props should not be accepted
1010
return <Heading backgroundColor="thistle" />
1111
}
12+
13+
export function shouldNotAcceptInvalidAsProp() {
14+
// @ts-expect-error as prop should not be accepted
15+
return <Heading as="p" />
16+
}

0 commit comments

Comments
 (0)