Skip to content

Instantly share code, notes, and snippets.

@eduardinni
Created May 9, 2024 07:45
Show Gist options
  • Save eduardinni/3208a4c8159f689950ec9995a04097c8 to your computer and use it in GitHub Desktop.
Save eduardinni/3208a4c8159f689950ec9995a04097c8 to your computer and use it in GitHub Desktop.
react-native-fontawesome FontAwesomeIcon typescript
import React, { ReactNode } from 'react';
import humps from 'humps';
import {
Svg,
Path,
Rect,
Defs,
Mask,
G,
ClipPath,
type NativeProps,
} from 'react-native-svg';
import { NativeComponentType } from 'react-native/Libraries/Utilities/codegenNativeComponent';
import type { AbstractElement } from '@fortawesome/fontawesome-svg-core';
function svgObjectMap(tag: string): NativeComponentType<NativeProps> {
let element;
switch (tag) {
case 'svg':
element = Svg as unknown;
break;
case 'path':
element = Path as unknown;
break;
case 'rect':
element = Rect as unknown;
break;
case 'defs':
element = Defs as unknown;
break;
case 'mask':
element = Mask as unknown;
break;
case 'g':
element = G as unknown;
break;
case 'clipPath':
element = ClipPath as unknown;
break;
default:
element = null;
}
return element as NativeComponentType<NativeProps>;
}
export default function convert(
createElement: typeof React.createElement,
element: AbstractElement,
): ReactNode {
if (typeof element === 'string') {
return element;
}
const children: ReactNode[] | undefined = (element.children || []).map(
(child: AbstractElement) => {
return convert(createElement, child) as ReactNode;
},
);
const mixins = Object.keys(element.attributes || {}).reduce(
(acc: { attrs: any }, key: string) => {
const val = element.attributes[key];
switch (key) {
case 'class':
case 'role':
case 'xmlns':
delete element.attributes[key];
break;
case 'focusable':
acc.attrs[key] = val === 'true';
break;
default:
if (
key.indexOf('aria-') === 0 ||
key.indexOf('data-') === 0 ||
(key === 'fill' && val === 'currentColor')
) {
delete element.attributes[key];
} else {
acc.attrs[humps.camelize(key)] = val;
}
}
return acc;
},
{ attrs: {} },
);
return createElement(
svgObjectMap(element.tag),
{ ...mixins.attrs },
...children,
);
}
import React from 'react';
import { StyleProp, StyleSheet } from 'react-native';
import { icon } from '@fortawesome/fontawesome-svg-core';
import convert from '../util/converter';
import type { IconDefinition } from '@fortawesome/fontawesome-common-types';
import type {
IconParams,
Transform,
IconLookup,
Styles,
AbstractElement,
} from '@fortawesome/fontawesome-svg-core';
export const DEFAULT_SIZE = 16;
export const DEFAULT_COLOR = '#000';
export const DEFAULT_SECONDARY_OPACITY = 0.4;
type FontAwesomeIconProps = {
icon: IconDefinition;
mask?: IconLookup;
maskId?: string;
transform?: Transform;
style?: StyleProp<object>;
color?: string | null;
secondaryColor?: string | null;
secondaryOpacity?: number | null;
size?: number;
};
export default function FontAwesomeIcon({
icon: iconArgs,
mask,
maskId,
transform,
style = {},
color = null,
secondaryColor = null,
secondaryOpacity = null,
size = DEFAULT_SIZE,
}: FontAwesomeIconProps) {
const styleFlatten: Styles = StyleSheet.flatten(style) as Styles;
const renderIconParams: IconParams = {
transform,
mask,
maskId,
};
const renderedIcon = icon(iconArgs as IconLookup, renderIconParams);
if (!renderedIcon) {
return null;
}
const { abstract } = renderedIcon;
// This is the color that will be passed to the "fill" prop of the Svg element
const resolvedColor = color || styleFlatten.color || DEFAULT_COLOR;
// This is the color that will be passed to the "fill" prop of the secondary Path element child (in Duotone Icons)
// `null` value will result in using the primary color, at 40% opacity
const resolvedSecondaryColor = secondaryColor || resolvedColor;
// Secondary layer opacity should default to 0.4, unless a specific opacity value or a specific secondary color was given
const resolvedSecondaryOpacity =
secondaryOpacity || DEFAULT_SECONDARY_OPACITY;
// To avoid confusion down the line, we'll remove properties from the StyleSheet, like color, that are being overridden
// or resolved in other ways, to avoid ambiguity as to which inputs cause which outputs in the underlying rendering process.
// In other words, we don't want color (for example) to be specified via two different inputs.
const { color: styleColor, ...modifiedStyle } = styleFlatten;
const resolvedHeight = size || DEFAULT_SIZE;
const resolvedWidth = size || DEFAULT_SIZE;
const rootAttributes = abstract[0].attributes;
rootAttributes.height = resolvedHeight;
rootAttributes.width = resolvedWidth;
rootAttributes.style = modifiedStyle;
replaceCurrentColor(
abstract[0],
resolvedColor,
resolvedSecondaryColor,
resolvedSecondaryOpacity,
);
return convertCurry(abstract[0]);
}
const convertCurry = convert.bind(null, React.createElement);
function replaceCurrentColor(
obj: AbstractElement,
primaryColor: string,
secondaryColor: string,
secondaryOpacity: number,
) {
obj.children?.forEach((child, _childIndex) => {
replaceFill(child, primaryColor, secondaryColor, secondaryOpacity);
if (Object.prototype.hasOwnProperty.call(child, 'attributes')) {
replaceFill(
child.attributes,
primaryColor,
secondaryColor,
secondaryOpacity,
);
}
if (Array.isArray(child.children) && child.children.length > 0) {
replaceCurrentColor(
child,
primaryColor,
secondaryColor,
secondaryOpacity,
);
}
});
}
function replaceFill(
obj: any,
primaryColor: string,
secondaryColor: string,
secondaryOpacity: number,
) {
if (hasPropertySetToValue(obj, 'fill', 'currentColor')) {
if (hasPropertySetToValue(obj, 'class', 'fa-primary')) {
obj.fill = primaryColor;
} else if (hasPropertySetToValue(obj, 'class', 'fa-secondary')) {
obj.fill = secondaryColor;
obj.fillOpacity = secondaryOpacity;
} else {
obj.fill = primaryColor;
}
}
}
function hasPropertySetToValue(obj: any, property: string, value: string) {
return (
Object.prototype.hasOwnProperty.call(obj, property) &&
obj[property] === value
);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment