Skip to content

Commit 2c84902

Browse files
committed
feat(layers): add support for layers with FontAwesomeLayers component
1 parent 663cd25 commit 2c84902

File tree

6 files changed

+237
-17
lines changed

6 files changed

+237
-17
lines changed

src/components/FontAwesomeIcon.tsx

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1-
import React from 'react'
1+
import React, { RefAttributes, SVGAttributes } from 'react'
22

33
import {
44
icon as faIcon,
55
parse as faParse,
66
} from '@fortawesome/fontawesome-svg-core'
77

8-
import { convert } from '../converter'
8+
import { makeReactConverter } from '../converter'
99
import { useAccessibilityId } from '../hooks/useAccessibilityId'
1010
import { Logger } from '../logger'
1111
import { FontAwesomeIconProps } from '../types/icon-props'
@@ -100,7 +100,11 @@ export const FontAwesomeIcon = React.forwardRef<
100100
}
101101

102102
const { abstract } = renderedIcon
103-
const extraProps: Partial<FontAwesomeIconProps> = { ref }
103+
const extraProps: Omit<
104+
SVGAttributes<SVGSVGElement>,
105+
'children' | 'mask' | 'transform'
106+
> &
107+
RefAttributes<SVGSVGElement> = { ref }
104108

105109
for (const key of typedObjectKeys(allProps)) {
106110
// Skip default props
@@ -115,9 +119,7 @@ export const FontAwesomeIcon = React.forwardRef<
115119
extraProps[key] = allProps[key]
116120
}
117121

118-
return convertCurry(abstract[0], extraProps)
122+
return makeReactConverter(abstract[0], extraProps)
119123
})
120124

121125
FontAwesomeIcon.displayName = 'FontAwesomeIcon'
122-
123-
const convertCurry = convert.bind(null, React.createElement)
Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
import React, { useMemo } from 'react'
2+
3+
import {
4+
counter as faCounter,
5+
text as faText,
6+
parse as faParse,
7+
Transform,
8+
SizeProp,
9+
} from '@fortawesome/fontawesome-svg-core'
10+
11+
import { makeReactConverter } from '../converter'
12+
import { CSSVariables } from '../types/css-variables'
13+
import { LAYER_CLASSES, STYLE_CLASSES } from '../utils/constants'
14+
import { withPrefix } from '../utils/get-class-list-from-props'
15+
16+
type Attributes = React.HTMLAttributes<HTMLSpanElement>
17+
18+
interface FontAwesomeLayersProps extends Attributes {
19+
children: React.ReactNode
20+
className?: string | undefined
21+
size?: SizeProp | undefined
22+
}
23+
24+
const DEFAULT_CLASSNAMES = `${LAYER_CLASSES.default} ${STYLE_CLASSES.fixedWidth}`
25+
26+
/**
27+
* React Component that allows you to stack multiple Font Awesome icons on top of each other,
28+
* or to layer with text or a counter.
29+
*
30+
* @see https://docs.fontawesome.com/web/style/layer
31+
*
32+
* @example
33+
* ```tsx
34+
* import { FontAwesomeIcon, FontAwesomeLayers } from '@fortawesome/react-fontawesome'
35+
* import { faBookmark, faCircle, faCheck, faHeart, faMoon, faPlay, faStar, faSun } from '@fortawesome/free-solid-svg-icons'
36+
*
37+
* // React versions of the examples from the FontAwesome Web Docs
38+
* export const Examples = () => (
39+
* <div className="fa-4x">
40+
* <FontAwesomeLayers>
41+
* <FontAwesomeIcon icon={faCircle} color="tomato" />
42+
* <FontAwesomeIcon icon={faCheck} inverse transform="shrink-6" />
43+
* </FontAwesomeLayers>
44+
* <FontAwesomeLayers>
45+
* <FontAwesomeIcon icon={faBookmark} />
46+
* <FontAwesomeIcon icon={faHeart} color="tomato" transform="shrink-10 up-2" />
47+
* </FontAwesomeLayers>
48+
* <FontAwesomeLayers>
49+
* <FontAwesomeIcon icon={faPlay} transform="rotate--90 grow-4" />
50+
* <FontAwesomeIcon icon={faSun} inverse transform="shrink-10 up-2" />
51+
* <FontAwesomeIcon icon={faMoon} inverse transform="shrink-11 down-4.2 left-4" />
52+
* <FontAwesomeIcon icon={faStar} inverse transform="shrink-11 down-4.2 right-4" />
53+
* </FontAwesomeLayers>
54+
* </div>
55+
* )
56+
* ```
57+
*
58+
* For examples using Text or Counter components:
59+
* @see {@link LayersText}
60+
* @see {@link LayersCounter}
61+
*/
62+
const FontAwesomeLayers = ({
63+
children,
64+
className,
65+
size,
66+
...attributes
67+
}: FontAwesomeLayersProps) => {
68+
const prefixedDefaultClasses = withPrefix(DEFAULT_CLASSNAMES)
69+
const classes = className
70+
? `${prefixedDefaultClasses} ${className}`
71+
: prefixedDefaultClasses
72+
73+
const element = (
74+
<span {...attributes} className={classes}>
75+
{children}
76+
</span>
77+
)
78+
79+
if (size) {
80+
return <div className={withPrefix(`fa-${size}`)}>{element}</div>
81+
}
82+
83+
return element
84+
}
85+
86+
/**
87+
* Text component to be used within a `FontAwesomeLayers` component.
88+
*
89+
* @see https://docs.fontawesome.com/web/style/layer
90+
*
91+
* @example
92+
* ```tsx
93+
* import { FontAwesomeLayers, LayersText } from '@fortawesome/react-fontawesome'
94+
* import { faCalendar, faCertificate } from '@fortawesome/free-solid-svg-icons'
95+
*
96+
* // React versions of the examples from the FontAwesome Web Docs
97+
* export const Examples = () => (
98+
* <div className="fa-4x">
99+
* <FontAwesomeLayers>
100+
* <FontAwesomeIcon icon={faCalendar} />
101+
* <LayersText
102+
* text="27"
103+
* inverse
104+
* style={{ fontWeight: '900' }}
105+
* transform="shrink-8 down-3"
106+
* />
107+
* </FontAwesomeLayers>
108+
* <FontAwesomeLayers>
109+
* <FontAwesomeIcon icon={faCertificate} />
110+
* <LayersText
111+
* text="NEW"
112+
* inverse
113+
* style={{ fontWeight: '900' }}
114+
* transform="shrink-11.5 rotate--30"
115+
* />
116+
* </FontAwesomeLayers>
117+
* </div>
118+
* )
119+
* ```
120+
*/
121+
const LayersText = ({
122+
text,
123+
className,
124+
inverse,
125+
transform,
126+
style,
127+
...attributes
128+
}: Attributes & {
129+
text: string
130+
className?: string | undefined
131+
inverse?: boolean | undefined
132+
transform?: string | Transform | undefined
133+
style?: (React.CSSProperties & CSSVariables) | undefined
134+
}) => {
135+
const textAbstractElement = useMemo(() => {
136+
const textObject = faText(text, {
137+
classes: [
138+
...(className?.split(' ') || []),
139+
...(inverse ? [STYLE_CLASSES.inverse] : []),
140+
],
141+
transform:
142+
typeof transform === 'string'
143+
? faParse.transform(transform)
144+
: transform,
145+
})
146+
147+
console.log(textObject.abstract[0])
148+
149+
return textObject.abstract[0]
150+
}, [text, transform, className, inverse])
151+
152+
return makeReactConverter(textAbstractElement, { ...attributes, style })
153+
}
154+
155+
/**
156+
* Counter component to be used within a `FontAwesomeLayers` component.
157+
*
158+
* @see https://docs.fontawesome.com/web/style/layer
159+
*
160+
* @example
161+
* ```tsx
162+
* import { FontAwesomeLayers, LayersCounter } from '@fortawesome/react-fontawesome'
163+
* import { faEnvelope } from '@fortawesome/free-solid-svg-icons'
164+
*
165+
* // React version of the example from the FontAwesome Web Docs
166+
* export const Example = ({ count = 1419 }) => (
167+
* <FontAwesomeLayers size="4x">
168+
* <FontAwesomeIcon icon={faEnvelope} />
169+
* <LayersCounter count={count.toLocaleString()} style={{ backgroundColor: 'tomato' }} />
170+
* </FontAwesomeLayers>
171+
* )
172+
* ```
173+
*/
174+
const LayersCounter = ({
175+
count,
176+
className,
177+
style,
178+
...attributes
179+
}: Attributes & {
180+
count: number | string
181+
className?: string | undefined
182+
style?: (React.CSSProperties & CSSVariables) | undefined
183+
}) => {
184+
const counterAbstractElement = useMemo(
185+
() =>
186+
faCounter(count, {
187+
classes: className?.split(' '),
188+
}).abstract[0],
189+
[count, className],
190+
)
191+
192+
return makeReactConverter(counterAbstractElement, { ...attributes, style })
193+
}
194+
195+
export { FontAwesomeLayers, LayersText, LayersCounter }

src/converter.ts

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
1-
import React, { type CSSProperties } from 'react'
1+
import React, {
2+
HTMLAttributes,
3+
RefAttributes,
4+
SVGAttributes,
5+
type CSSProperties,
6+
} from 'react'
27

38
import type { AbstractElement } from '@fortawesome/fontawesome-svg-core'
49

5-
import type { FontAwesomeIconProps } from './types/icon-props'
610
import { camelize } from './utils/camelize'
711

812
function capitalize(val: string): string {
@@ -68,12 +72,15 @@ type AttributesOverride = Record<string, unknown> & {
6872
style?: React.CSSProperties
6973
}
7074

71-
export function convert(
75+
export function convert<
76+
El extends Element = SVGSVGElement,
77+
Attr extends HTMLAttributes<El> = SVGAttributes<El>,
78+
>(
7279
createElement: typeof React.createElement,
7380
element: Omit<AbstractElement, 'attributes'> & {
7481
attributes: AttributesOverride
7582
},
76-
extraProps: Partial<FontAwesomeIconProps> = {},
83+
extraProps: Attr & RefAttributes<El> = {} as Attr & RefAttributes<El>,
7784
): React.JSX.Element {
7885
if (typeof element === 'string') {
7986
return element
@@ -91,7 +98,6 @@ export function convert(
9198
switch (true) {
9299
case key === 'class': {
93100
attrs.className = val
94-
delete elementAttributes.class
95101
break
96102
}
97103
case key === 'style': {
@@ -130,3 +136,5 @@ export function convert(
130136

131137
return createElement(element.tag, { ...remaining, ...attrs }, ...children)
132138
}
139+
140+
export const makeReactConverter = convert.bind(null, React.createElement)

src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
export * from './components/FontAwesomeIcon'
2+
export * from './components/FontAwesomeLayers'
23
export * from './types/animation-props'
34
export * from './types/icon-props'
45
export * from './types/transform-props'

src/utils/constants.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ export const SVG_CORE_VERSION =
1818
// Cache the version check result since it never changes during runtime
1919
export const IS_VERSION_7_OR_LATER = Number.parseInt(SVG_CORE_VERSION) >= 7
2020

21+
export const DEFAULT_CLASSNAME_PREFIX = 'fa'
22+
2123
export const ANIMATION_CLASSES = {
2224
beat: 'fa-beat',
2325
fade: 'fa-fade',
@@ -79,3 +81,7 @@ export const STYLE_CLASSES = {
7981
swapOpacity: 'fa-swap-opacity',
8082
widthAuto: 'fa-width-auto',
8183
} as const
84+
85+
export const LAYER_CLASSES = {
86+
default: 'fa-layers',
87+
} as const

src/utils/get-class-list-from-props.ts

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,19 @@ import {
77
SIZE_CLASSES,
88
STYLE_CLASSES,
99
IS_VERSION_7_OR_LATER,
10+
DEFAULT_CLASSNAME_PREFIX,
1011
} from './constants'
1112
import { FontAwesomeIconProps } from '../types/icon-props'
1213

13-
function withCustomPrefix(cls: string): string {
14-
const customPrefix = config.cssPrefix || config.familyPrefix || 'fa'
15-
return customPrefix === 'fa' ? cls : cls.replace('fa-', `${customPrefix}-`)
14+
export function withPrefix(cls: string): string {
15+
const prefix =
16+
config.cssPrefix || config.familyPrefix || DEFAULT_CLASSNAME_PREFIX
17+
return prefix === DEFAULT_CLASSNAME_PREFIX
18+
? cls
19+
: cls.replaceAll(
20+
new RegExp(`(?<=^|\\s)${DEFAULT_CLASSNAME_PREFIX}-`, 'g'),
21+
`${prefix}-`,
22+
)
1623
}
1724

1825
/**
@@ -87,11 +94,12 @@ export function getClassListFromProps(props: FontAwesomeIconProps): string[] {
8794
if (rotateBy) result.push(STYLE_CLASSES.rotateBy)
8895
if (widthAuto) result.push(STYLE_CLASSES.widthAuto)
8996

90-
const prefix = config.cssPrefix || config.familyPrefix || 'fa'
97+
const prefix =
98+
config.cssPrefix || config.familyPrefix || DEFAULT_CLASSNAME_PREFIX
9199

92-
return prefix === 'fa'
100+
return prefix === DEFAULT_CLASSNAME_PREFIX
93101
? result
94102
: // TODO: see if we can achieve custom prefix support without iterating
95103
// eslint-disable-next-line unicorn/no-array-callback-reference
96-
result.map(withCustomPrefix)
104+
result.map(withPrefix)
97105
}

0 commit comments

Comments
 (0)