Skip to content

Commit 09fbffc

Browse files
committed
feat: better prop definitions, fix accessibility ids for React SSR
1 parent 979c817 commit 09fbffc

15 files changed

+787
-301
lines changed

eslint.config.mjs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,18 @@ const config = tseslint.config(
9292
],
9393
},
9494
},
95+
{
96+
files: ['**/hooks/**/*.ts', '**/use*.ts'],
97+
98+
rules: {
99+
'unicorn/filename-case': [
100+
'error',
101+
{
102+
case: 'camelCase',
103+
},
104+
],
105+
},
106+
},
95107
)
96108

97109
export default config

package-lock.json

Lines changed: 236 additions & 109 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 19 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@
3737
"license": "MIT",
3838
"scripts": {
3939
"build": "tsup",
40-
"dist": "cross-env NODE_ENV=production npm run build",
40+
"dist": "npm run clean && cross-env NODE_ENV=production npm run build",
4141
"lint": "eslint .",
4242
"format:check": "prettier --check .",
4343
"format:fix": "prettier --write .",
@@ -49,7 +49,7 @@
4949
"install.5": "npm --no-save install @fortawesome/[email protected] @fortawesome/[email protected]",
5050
"install.6": "npm --no-save install @fortawesome/[email protected] @fortawesome/[email protected]",
5151
"install.7": "npm --no-save install @fortawesome/[email protected] @fortawesome/[email protected]",
52-
"clean": "rm -rf dist"
52+
"clean": "rimraf dist"
5353
},
5454
"dependencies": {
5555
"semver": "^7.7.2"
@@ -64,29 +64,29 @@
6464
"@babel/preset-env": "^7.28.0",
6565
"@babel/preset-react": "^7.27.1",
6666
"@babel/preset-typescript": "^7.27.1",
67-
"@eslint/js": "^9.32.0",
67+
"@eslint/js": "^9.33.0",
6868
"@fortawesome/fontawesome-svg-core": "^7.0.0",
6969
"@fortawesome/free-brands-svg-icons": "^7.0.0",
7070
"@fortawesome/free-solid-svg-icons": "^7.0.0",
7171
"@testing-library/dom": "^10.4.1",
7272
"@testing-library/jest-dom": "^6.6.4",
7373
"@testing-library/react": "^16.3.0",
7474
"@types/jest": "^30.0.0",
75-
"@types/react": "^19.1.9",
75+
"@types/react": "^19.1.10",
7676
"@types/react-dom": "^19.1.7",
7777
"@types/semver": "^7.7.0",
78-
"browserslist": "^4.25.1",
79-
"caniuse-lite": "^1.0.30001731",
78+
"browserslist": "^4.25.2",
79+
"caniuse-lite": "^1.0.30001734",
8080
"cross-env": "^10.0.0",
81-
"eslint": "^9.32.0",
81+
"eslint": "^9.33.0",
8282
"eslint-config-prettier": "^10.1.8",
8383
"eslint-plugin-import": "^2.32.0",
8484
"eslint-plugin-jest": "^29.0.1",
8585
"eslint-plugin-jsx-a11y": "^6.10.2",
86-
"eslint-plugin-prettier": "^5.5.3",
86+
"eslint-plugin-prettier": "^5.5.4",
8787
"eslint-plugin-react": "^7.37.5",
8888
"eslint-plugin-react-hooks": "^5.2.0",
89-
"eslint-plugin-testing-library": "^7.6.3",
89+
"eslint-plugin-testing-library": "^7.6.6",
9090
"eslint-plugin-unicorn": "^60.0.0",
9191
"globals": "^16.3.0",
9292
"husky": "^9.1.7",
@@ -96,26 +96,26 @@
9696
"pretty-quick": "^4.2.2",
9797
"react": "^19.1.1",
9898
"react-dom": "^19.1.1",
99+
"rimraf": "^6.0.1",
99100
"ts-jest": "^29.4.1",
100101
"ts-node": "^10.9.2",
101102
"tsup": "^8.5.0",
102103
"typescript": "^5.9.2",
103-
"typescript-eslint": "^8.39.0"
104+
"typescript-eslint": "^8.39.1"
104105
},
105106
"files": [
106107
"./dist",
107-
"CHANGELOG.md",
108-
"CODE_OF_CONDUCT.md",
109-
"CONTRIBUTING.md",
110-
"DEVELOPMENT.md",
111-
"LICENSE.txt",
112-
"README.md",
113-
"UPGRADING.md"
108+
"./CHANGELOG.md",
109+
"./CODE_OF_CONDUCT.md",
110+
"./CONTRIBUTING.md",
111+
"./DEVELOPMENT.md",
112+
"./LICENSE.txt",
113+
"./README.md",
114+
"./UPGRADING.md"
114115
],
115116
"browserslist": [
116117
"> 1%",
117-
"last 2 versions",
118-
"ie > 10"
118+
"last 2 versions"
119119
],
120120
"husky": {
121121
"hooks": {

src/components/FontAwesomeIcon.tsx

Lines changed: 70 additions & 166 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,14 @@
1-
import React, { CSSProperties, RefAttributes, SVGAttributes } from 'react'
1+
import React from 'react'
22

33
import {
44
icon as faIcon,
55
parse as faParse,
66
} from '@fortawesome/fontawesome-svg-core'
7-
import type {
8-
FaSymbol,
9-
FlipProp,
10-
IconProp,
11-
PullProp,
12-
RotateProp,
13-
SizeProp,
14-
Transform,
15-
} from '@fortawesome/fontawesome-svg-core'
167

178
import { convert } from '../converter'
9+
import { useAccessibilityId } from '../hooks/useAccessibilityId'
1810
import { Logger } from '../logger'
11+
import { FontAwesomeIconProps } from '../types/icon-props'
1912
import { getClassListFromProps } from '../utils/get-class-list-from-props'
2013
import { normalizeIconArgs } from '../utils/normalize-icon-args'
2114
import { typedObjectKeys } from '../utils/typed-object-keys'
@@ -51,168 +44,79 @@ const DEFAULT_PROPS = {
5144
transform: undefined,
5245
swapOpacity: false,
5346
widthAuto: false,
54-
}
47+
} as const satisfies Partial<FontAwesomeIconProps>
5548

5649
const DEFAULT_PROP_KEYS = new Set(Object.keys(DEFAULT_PROPS))
5750

58-
export interface FontAwesomeIconProps
59-
extends Omit<SVGAttributes<SVGSVGElement>, 'children' | 'mask' | 'transform'>,
60-
RefAttributes<SVGSVGElement> {
61-
/**
62-
* The icon to render.
63-
* @see {@link https://docs.fontawesome.com/web/use-with/react/add-icons}
64-
*/
65-
icon: IconProp
66-
/**
67-
* Grab the Mask utility when you want to layer two icons but have the inner icon cut out from the icon below so the parent element’s background shows through.
68-
* @see {@link https://docs.fontawesome.com/web/use-with/react/style#mask}
69-
*/
70-
mask?: IconProp | undefined
71-
maskId?: string | undefined
72-
className?: string | undefined
73-
color?: string | undefined
74-
spin?: boolean | undefined
75-
spinPulse?: boolean | undefined
76-
spinReverse?: boolean | undefined
77-
pulse?: boolean | undefined
78-
beat?: boolean | undefined
79-
fade?: boolean | undefined
80-
beatFade?: boolean | undefined
81-
bounce?: boolean | undefined
82-
shake?: boolean | undefined
83-
border?: boolean | undefined
84-
/**
85-
* @deprecated
86-
* @since 7.0.0
87-
*
88-
* Starting in FontAwesome 7.0.0, all icons are fixed width by default.
89-
* This property will be removed in a future version.
90-
*
91-
* If you want to remove the fixed width to replicate the behavior of
92-
* previous versions, you can set the new `widthAuto` property to `true`.
93-
*
94-
* @see {@link FontAwesomeIconProps.widthAuto}
95-
*/
96-
fixedWidth?: boolean | undefined
97-
inverse?: boolean | undefined
98-
listItem?: boolean | undefined
99-
flip?: FlipProp | boolean | undefined
100-
size?: SizeProp | undefined
101-
pull?: PullProp | undefined
102-
/**
103-
* The rotation property is used to rotate the icon by 90, 180, or 270 degrees.
104-
*
105-
* @see {@link https://docs.fontawesome.com/web/use-with/react/style#rotation}
106-
*/
107-
rotation?: RotateProp | undefined
108-
/**
109-
* Custom rotation is used to rotate the icon by a specific number of degrees,
110-
* rather than the standard 90, 180, or 270 degrees available in the `rotation` property.
111-
*
112-
* To use this feature, set `rotateBy` to `true` and provide a CSS variable `--fa-rotate-angle`
113-
* with the desired rotation angle in degrees.
114-
*
115-
* @example
116-
* ```tsx
117-
* <FontAwesomeIcon
118-
* icon="fa-solid fa-coffee"
119-
* rotateBy
120-
* style={{ '--fa-rotate-angle': '329deg' }}
121-
* />
122-
* ```
123-
*
124-
* @see {@link https://docs.fontawesome.com/web/use-with/react/style#custom-rotation}
125-
* @since 7.0.0
126-
*/
127-
rotateBy?: boolean | undefined
128-
transform?: string | Transform | undefined
129-
symbol?: FaSymbol | undefined
130-
style?: CSSProperties | undefined
131-
tabIndex?: number | undefined
132-
title?: string | undefined
133-
titleId?: string | undefined
134-
/**
135-
* When using Duotone icons, this property will swap the opacity of the two colors.
136-
* The first color will be rendered with the opacity of the second color, and vice versa
137-
*
138-
* @see {@link https://docs.fontawesome.com/web/use-with/react/style#duotone-icons}
139-
*/
140-
swapOpacity?: boolean | undefined
141-
/**
142-
* When set to `true`, the icon will automatically adjust its width to
143-
* only the interior symbol and not the entire Icon Canvas.
144-
*
145-
* @see {@link https://docs.fontawesome.com/web/style/icon-canvas}
146-
* @since 7.0.0
147-
*/
148-
widthAuto?: boolean | undefined
149-
}
150-
151-
export const FontAwesomeIcon = React.forwardRef(
152-
(
153-
props: FontAwesomeIconProps,
154-
ref: React.Ref<SVGSVGElement>,
155-
): React.JSX.Element | null => {
156-
const allProps: FontAwesomeIconProps = { ...DEFAULT_PROPS, ...props }
157-
158-
const {
159-
icon: iconArgs,
160-
mask: maskArgs,
161-
symbol,
162-
title,
163-
titleId,
164-
maskId,
165-
transform,
166-
} = allProps
167-
168-
const iconLookup = normalizeIconArgs(iconArgs)
169-
170-
if (!iconLookup) {
171-
logger.error('Icon lookup is undefined', iconArgs)
172-
return null
173-
}
174-
175-
const classList = getClassListFromProps(allProps)
176-
177-
const transformProps =
178-
typeof transform === 'string' ? faParse.transform(transform) : transform
179-
180-
const normalizedMaskArgs = normalizeIconArgs(maskArgs)
181-
182-
const renderedIcon = faIcon(iconLookup, {
183-
...(classList.length > 0 && { classes: classList }),
184-
...(transformProps && { transform: transformProps }),
185-
...(normalizedMaskArgs && { mask: normalizedMaskArgs }),
186-
symbol,
187-
title,
188-
titleId,
189-
maskId,
190-
})
191-
192-
if (!renderedIcon) {
193-
logger.error('Could not find icon', iconLookup)
194-
return null
51+
/**
52+
* FontAwesomeIcon component.
53+
*/
54+
export const FontAwesomeIcon = React.forwardRef<
55+
SVGSVGElement,
56+
FontAwesomeIconProps
57+
>((props, ref): React.JSX.Element | null => {
58+
const allProps: FontAwesomeIconProps = { ...DEFAULT_PROPS, ...props }
59+
60+
const {
61+
icon: iconArgs,
62+
mask: maskArgs,
63+
symbol,
64+
title,
65+
titleId: titleIdFromProps,
66+
maskId: maskIdFromProps,
67+
transform,
68+
} = allProps
69+
70+
const maskId = useAccessibilityId(maskIdFromProps, Boolean(maskArgs))
71+
const titleId = useAccessibilityId(titleIdFromProps, Boolean(title))
72+
73+
const iconLookup = normalizeIconArgs(iconArgs)
74+
75+
if (!iconLookup) {
76+
logger.error('Icon lookup is undefined', iconArgs)
77+
return null
78+
}
79+
80+
const classList = getClassListFromProps(allProps)
81+
82+
const transformProps =
83+
typeof transform === 'string' ? faParse.transform(transform) : transform
84+
85+
const normalizedMaskArgs = normalizeIconArgs(maskArgs)
86+
87+
const renderedIcon = faIcon(iconLookup, {
88+
...(classList.length > 0 && { classes: classList }),
89+
...(transformProps && { transform: transformProps }),
90+
...(normalizedMaskArgs && { mask: normalizedMaskArgs }),
91+
symbol,
92+
title,
93+
titleId,
94+
maskId,
95+
})
96+
97+
if (!renderedIcon) {
98+
logger.error('Could not find icon', iconLookup)
99+
return null
100+
}
101+
102+
const { abstract } = renderedIcon
103+
const extraProps: Partial<FontAwesomeIconProps> = { ref }
104+
105+
for (const key of typedObjectKeys(allProps)) {
106+
// Skip default props
107+
if (DEFAULT_PROP_KEYS.has(key)) {
108+
continue
195109
}
196110

197-
const { abstract } = renderedIcon
198-
const extraProps: Partial<FontAwesomeIconProps> = { ref }
199-
200-
for (const key of typedObjectKeys(allProps)) {
201-
// Skip default props
202-
if (DEFAULT_PROP_KEYS.has(key)) {
203-
continue
204-
}
205-
206-
// Add all other props to the extraProps object
207-
// @ts-expect-error since `key` can be any of the keys in FontAwesomeIconProps,
208-
// TypeScript widens the type of the `obj[key]` lookups to a union of all possible values,
209-
// which will not correctly overlap each other.
210-
extraProps[key] = allProps[key]
211-
}
111+
// Add all other props to the extraProps object
112+
// @ts-expect-error since `key` can be any of the keys in FontAwesomeIconProps,
113+
// TypeScript widens the type of the `obj[key]` lookups to a union of all possible values,
114+
// which will not correctly overlap each other.
115+
extraProps[key] = allProps[key]
116+
}
212117

213-
return convertCurry(abstract[0], extraProps)
214-
},
215-
)
118+
return convertCurry(abstract[0], extraProps)
119+
})
216120

217121
FontAwesomeIcon.displayName = 'FontAwesomeIcon'
218122

src/converter.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
import React, { CSSProperties } from 'react'
1+
import React, { type CSSProperties } from 'react'
22

3-
import { AbstractElement } from '@fortawesome/fontawesome-svg-core'
3+
import type { AbstractElement } from '@fortawesome/fontawesome-svg-core'
44

5-
import { FontAwesomeIconProps } from './components/FontAwesomeIcon'
5+
import type { FontAwesomeIconProps } from './types/icon-props'
66
import { camelize } from './utils/camelize'
77

88
function capitalize(val: string): string {

0 commit comments

Comments
 (0)