Skip to content

Commit c938dab

Browse files
refactor(mobile): extract useResolvedGlyph hook to share context logic between Icon and TextIcon
Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 981ea10 commit c938dab

2 files changed

Lines changed: 31 additions & 32 deletions

File tree

Lines changed: 17 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { memo, useContext, useMemo } from 'react';
1+
import React, { memo, useMemo } from 'react';
22
import { Animated, Text } from 'react-native';
33
import type { StyleProp, TextStyle } from 'react-native';
44
import type { IconName } from '@coinbase/cds-common/types/IconName';
@@ -8,7 +8,7 @@ import { isDevelopment } from '@coinbase/cds-utils';
88
import { useTheme } from '../hooks/useTheme';
99

1010
import type { IconProps } from './Icon';
11-
import { DEFAULT_ICON_FONT_FAMILY, getIconSourceSize, IconGlyphSourceContext } from './createIcon';
11+
import { DEFAULT_ICON_FONT_FAMILY, getIconSourceSize, useResolvedGlyph } from './createIcon';
1212

1313
export type TextIconProps = Pick<IconProps, 'color' | 'size' | 'testID'> & {
1414
name: IconName;
@@ -23,6 +23,10 @@ export type TextIconProps = Pick<IconProps, 'color' | 'size' | 'testID'> & {
2323
style?: StyleProp<TextStyle>;
2424
}
2525
);
26+
27+
/** Stable bound source so TextIcon participates in the same context resolution as Icon. */
28+
const cdsGlyphSource = { glyphMap, fontFamily: DEFAULT_ICON_FONT_FAMILY };
29+
2630
/**
2731
*
2832
* This is a simplified, text-only version of the Icon component.
@@ -41,56 +45,39 @@ export const TextIcon = memo(function TextIcon({
4145
const theme = useTheme();
4246
const Component = animated ? Animated.Text : Text;
4347
const iconSize = theme.iconSize[size];
44-
const sourceSize = getIconSourceSize(iconSize);
4548
const iconColor = theme.color[color];
4649

47-
const contextSource = useContext(IconGlyphSourceContext);
48-
49-
const iconKey = `${name}-${sourceSize}-${active ? 'active' : 'inactive'}`;
50-
51-
// Context source (e.g. retail-icons override) takes priority over the CDS glyphMap.
52-
const contextGlyph = contextSource
53-
? contextSource.getGlyph
54-
? contextSource.getGlyph({
55-
glyphMap: contextSource.glyphMap,
56-
name,
57-
size,
58-
pixelSize: iconSize,
59-
active: Boolean(active),
60-
})
61-
: contextSource.glyphMap[iconKey as keyof typeof contextSource.glyphMap]
62-
: undefined;
63-
64-
const glyph = contextGlyph ?? glyphMap[iconKey as keyof typeof glyphMap];
65-
const fontFamily =
66-
contextGlyph !== undefined
67-
? (contextSource?.fontFamily ?? DEFAULT_ICON_FONT_FAMILY)
68-
: DEFAULT_ICON_FONT_FAMILY;
50+
const resolved = useResolvedGlyph(cdsGlyphSource, {
51+
name,
52+
size,
53+
pixelSize: iconSize,
54+
active: Boolean(active),
55+
});
6956

7057
const styles = useMemo(
7158
() =>
7259
[
7360
{
74-
fontFamily,
61+
fontFamily: resolved?.fontFamily,
7562
fontSize: iconSize,
7663
color: iconColor,
7764
},
7865
style,
7966
// TODO https://linear.app/coinbase/issue/CDS-1518/audit-potentially-harmful-reactnative-animated-pattern
8067
] as StyleProp<TextStyle>,
81-
[style, iconColor, iconSize, fontFamily],
68+
[style, iconColor, iconSize, resolved?.fontFamily],
8269
);
8370

84-
if (glyph === undefined) {
71+
if (resolved === undefined) {
8572
if (isDevelopment()) {
86-
console.error(`Unable to find glyph for icon name "${name}" with glyph key "${iconKey}"`);
73+
console.error(`Unable to find glyph for icon name "${name}" at size "${size}"`);
8774
}
8875
return null;
8976
}
9077

9178
return (
9279
<Component accessibilityRole="image" style={styles} testID={testID}>
93-
{glyph}
80+
{resolved.char}
9481
</Component>
9582
);
9683
});

‎packages/mobile/src/icons/createIcon.tsx‎

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,19 @@ const resolveGlyph = (
161161
return fromContext ?? resolveFromSource(boundSource, args);
162162
};
163163

164+
/**
165+
* Resolves a glyph for `name` against the nearest `IconGlyphSourceProvider`,
166+
* falling back to `boundSource` when the context has no match.
167+
* Extracts the shared lookup logic so both `createIcon` and `TextIcon` use it.
168+
*/
169+
export function useResolvedGlyph(
170+
boundSource: IconGlyphSource<any>,
171+
args: Omit<IconGlyphResolverArgs<string>, 'glyphMap'>,
172+
): ResolvedGlyph | undefined {
173+
const contextSource = useContext(IconGlyphSourceContext);
174+
return resolveGlyph(contextSource, boundSource, args);
175+
}
176+
164177
/** Creates a typed `Icon` component bound to an icon set. */
165178
export function createIcon<Name extends string>(source: IconGlyphSource<Name>) {
166179
const Icon = memo(({ ref, ..._props }: IconProps<Name> & { ref?: React.Ref<Text> }) => {
@@ -197,8 +210,7 @@ export function createIcon<Name extends string>(source: IconGlyphSource<Name>) {
197210
const finalColor = dangerouslySetColor ?? iconColor;
198211

199212
// Tried before the bound set, so a source can override a built-in icon.
200-
const contextSource = useContext(IconGlyphSourceContext);
201-
const resolved = resolveGlyph(contextSource, source, {
213+
const resolved = useResolvedGlyph(source, {
202214
name,
203215
size,
204216
pixelSize: iconSize,

0 commit comments

Comments
 (0)