diff --git a/packages/common/CHANGELOG.md b/packages/common/CHANGELOG.md index de05f1630a..6e8c536468 100644 --- a/packages/common/CHANGELOG.md +++ b/packages/common/CHANGELOG.md @@ -8,6 +8,10 @@ All notable changes to this project will be documented in this file. +## 9.23.0 ((8/26/2026, 09:18 AM PST)) + +This is an artificial version bump with no new change. + ## 9.22.0 ((8/25/2026, 08:11 AM PST)) This is an artificial version bump with no new change. diff --git a/packages/common/package.json b/packages/common/package.json index 4c4ea85c88..17536768a5 100644 --- a/packages/common/package.json +++ b/packages/common/package.json @@ -1,6 +1,6 @@ { "name": "@coinbase/cds-common", - "version": "9.22.0", + "version": "9.23.0", "description": "Coinbase Design System - Common", "repository": { "type": "git", diff --git a/packages/mcp-server/CHANGELOG.md b/packages/mcp-server/CHANGELOG.md index 8924eb86ba..bbfc2dca84 100644 --- a/packages/mcp-server/CHANGELOG.md +++ b/packages/mcp-server/CHANGELOG.md @@ -8,6 +8,10 @@ All notable changes to this project will be documented in this file. +## 9.23.0 ((8/26/2026, 09:18 AM PST)) + +This is an artificial version bump with no new change. + ## 9.22.0 ((8/25/2026, 08:11 AM PST)) This is an artificial version bump with no new change. diff --git a/packages/mcp-server/package.json b/packages/mcp-server/package.json index a82c527b18..553f38ce95 100644 --- a/packages/mcp-server/package.json +++ b/packages/mcp-server/package.json @@ -1,6 +1,6 @@ { "name": "@coinbase/cds-mcp-server", - "version": "9.22.0", + "version": "9.23.0", "description": "Coinbase Design System - MCP Server", "repository": { "type": "git", diff --git a/packages/mobile/CHANGELOG.md b/packages/mobile/CHANGELOG.md index 756e8451a2..35b62756a1 100644 --- a/packages/mobile/CHANGELOG.md +++ b/packages/mobile/CHANGELOG.md @@ -8,6 +8,12 @@ All notable changes to this project will be documented in this file. +## 9.23.0 (8/26/2026 PST) + +#### 🚀 Updates + +- Let TextIcon respect IconGlyphSourceContext overrides. [[#862](https://github.com/coinbase/cds/pull/862)] + ## 9.22.0 (8/25/2026 PST) #### 🚀 Updates diff --git a/packages/mobile/package.json b/packages/mobile/package.json index 824e0d0ffe..87cf42ab33 100644 --- a/packages/mobile/package.json +++ b/packages/mobile/package.json @@ -1,6 +1,6 @@ { "name": "@coinbase/cds-mobile", - "version": "9.22.0", + "version": "9.23.0", "description": "Coinbase Design System - Mobile", "repository": { "type": "git", diff --git a/packages/mobile/src/icons/TextIcon.tsx b/packages/mobile/src/icons/TextIcon.tsx index 107b671db3..f3b3b03aee 100644 --- a/packages/mobile/src/icons/TextIcon.tsx +++ b/packages/mobile/src/icons/TextIcon.tsx @@ -8,7 +8,7 @@ import { isDevelopment } from '@coinbase/cds-utils'; import { useTheme } from '../hooks/useTheme'; import type { IconProps } from './Icon'; -import { getIconSourceSize } from './Icon'; +import { DEFAULT_ICON_FONT_FAMILY, getIconSourceSize, useResolvedGlyph } from './createIcon'; export type TextIconProps = Pick & { name: IconName; @@ -23,6 +23,10 @@ export type TextIconProps = Pick & { style?: StyleProp; } ); + +/** Stable bound source so TextIcon participates in the same context resolution as Icon. */ +const cdsGlyphSource = { glyphMap, fontFamily: DEFAULT_ICON_FONT_FAMILY }; + /** * * This is a simplified, text-only version of the Icon component. @@ -41,36 +45,39 @@ export const TextIcon = memo(function TextIcon({ const theme = useTheme(); const Component = animated ? Animated.Text : Text; const iconSize = theme.iconSize[size]; - const sourceSize = getIconSourceSize(iconSize); const iconColor = theme.color[color]; + const resolved = useResolvedGlyph(cdsGlyphSource, { + name, + size, + pixelSize: iconSize, + active: Boolean(active), + }); + const styles = useMemo( () => [ { - fontFamily: 'CoinbaseIcons', + fontFamily: resolved?.fontFamily, fontSize: iconSize, color: iconColor, }, style, // TODO https://linear.app/coinbase/issue/CDS-1518/audit-potentially-harmful-reactnative-animated-pattern ] as StyleProp, - [style, iconColor, iconSize], + [style, iconColor, iconSize, resolved?.fontFamily], ); - const iconName = `${name}-${sourceSize}-${active ? 'active' : 'inactive'}`; - const glyph = glyphMap[iconName as keyof typeof glyphMap]; - - if (glyph === undefined) { + if (resolved === undefined) { if (isDevelopment()) { - console.error(`Unable to find glyph for icon name "${name}" with glyph key "${iconName}"`); + console.error(`Unable to find glyph for icon name "${name}" at size "${size}"`); } return null; } return ( - {glyph} + {resolved.char} ); }); diff --git a/packages/mobile/src/icons/__tests__/TextIcon.test.tsx b/packages/mobile/src/icons/__tests__/TextIcon.test.tsx new file mode 100644 index 0000000000..c1d88afcec --- /dev/null +++ b/packages/mobile/src/icons/__tests__/TextIcon.test.tsx @@ -0,0 +1,97 @@ +import type { IconName } from '@coinbase/cds-common/types/IconName'; +import { render, screen } from '@testing-library/react-native'; + +import { DefaultThemeProvider } from '../../utils/testHelpers'; +import { DEFAULT_ICON_FONT_FAMILY, type GlyphMap, IconGlyphSourceProvider } from '../createIcon'; +import { TextIcon } from '../TextIcon'; + +const INACTIVE_GLYPH = '\u2606'; // ☆ +const ACTIVE_GLYPH = '\u2605'; // ★ +const OTHER_GLYPH = '\u25B2'; // ▲ + +type DemoIconName = 'star'; + +const demoGlyphMap: GlyphMap = { + 'star-12-active': ACTIVE_GLYPH, + 'star-12-inactive': INACTIVE_GLYPH, + 'star-16-active': ACTIVE_GLYPH, + 'star-16-inactive': INACTIVE_GLYPH, + 'star-24-active': ACTIVE_GLYPH, + 'star-24-inactive': INACTIVE_GLYPH, +}; + +// TextIcon reads directly from the CDS glyphMap module; mock it so tests are +// independent of the published icon set. +jest.mock('@coinbase/cds-icons/glyphMap', () => ({ + glyphMap: { + 'star-12-active': '\u2605', + 'star-12-inactive': '\u2606', + 'star-16-active': '\u2605', + 'star-16-inactive': '\u2606', + 'star-24-active': '\u2605', + 'star-24-inactive': '\u2606', + }, +})); + +const renderTextIcon = (ui: React.ReactElement) => + render({ui}); + +describe('TextIcon', () => { + it('renders the CDS glyph by default', () => { + renderTextIcon(); + expect(screen.getByText(INACTIVE_GLYPH)).toBeTruthy(); + }); + + it('uses the default CDS font family', () => { + renderTextIcon(); + expect(screen.getByText(INACTIVE_GLYPH)).toHaveStyle({ fontFamily: DEFAULT_ICON_FONT_FAMILY }); + }); + + it('uses the glyph and font family from the context source when the name matches', () => { + renderTextIcon( + + + , + ); + + expect(screen.getByText(OTHER_GLYPH)).toHaveStyle({ fontFamily: 'RetailIcons' }); + expect(screen.queryByText(INACTIVE_GLYPH)).toBeNull(); + }); + + it('falls back to the CDS glyphMap when the context source does not cover the name', () => { + renderTextIcon( + + + , + ); + + expect(screen.getByText(INACTIVE_GLYPH)).toHaveStyle({ + fontFamily: DEFAULT_ICON_FONT_FAMILY, + }); + }); + + it('uses a custom getGlyph resolver from the context source', () => { + const getGlyph = jest.fn(() => OTHER_GLYPH); + renderTextIcon( + + + , + ); + + expect(screen.getByText(OTHER_GLYPH)).toHaveStyle({ fontFamily: 'CustomFont' }); + expect(getGlyph).toHaveBeenCalledWith(expect.objectContaining({ name: 'star', active: false })); + }); + + it('returns null when no glyph is found in either the context source or CDS glyphMap', () => { + const consoleError = jest.spyOn(console, 'error').mockImplementation(() => undefined); + renderTextIcon(); + expect(screen.queryByRole('image')).toBeNull(); + consoleError.mockRestore(); + }); +}); diff --git a/packages/mobile/src/icons/createIcon.tsx b/packages/mobile/src/icons/createIcon.tsx index 7655c9caa3..6cfaa3253e 100644 --- a/packages/mobile/src/icons/createIcon.tsx +++ b/packages/mobile/src/icons/createIcon.tsx @@ -52,7 +52,7 @@ export type IconGlyphSource = { getGlyph?: (args: IconGlyphResolverArgs) => string | undefined; }; -const IconGlyphSourceContext = createContext | undefined>(undefined); +export const IconGlyphSourceContext = createContext | undefined>(undefined); export type IconGlyphSourceProviderProps = { /** @@ -161,6 +161,19 @@ const resolveGlyph = ( return fromContext ?? resolveFromSource(boundSource, args); }; +/** + * Resolves a glyph for `name` against the nearest `IconGlyphSourceProvider`, + * falling back to `boundSource` when the context has no match. + * Extracts the shared lookup logic so both `createIcon` and `TextIcon` use it. + */ +export function useResolvedGlyph( + boundSource: IconGlyphSource, + args: Omit, 'glyphMap'>, +): ResolvedGlyph | undefined { + const contextSource = useContext(IconGlyphSourceContext); + return resolveGlyph(contextSource, boundSource, args); +} + /** Creates a typed `Icon` component bound to an icon set. */ export function createIcon(source: IconGlyphSource) { const Icon = memo(({ ref, ..._props }: IconProps & { ref?: React.Ref }) => { @@ -197,8 +210,7 @@ export function createIcon(source: IconGlyphSource) { const finalColor = dangerouslySetColor ?? iconColor; // Tried before the bound set, so a source can override a built-in icon. - const contextSource = useContext(IconGlyphSourceContext); - const resolved = resolveGlyph(contextSource, source, { + const resolved = useResolvedGlyph(source, { name, size, pixelSize: iconSize, diff --git a/packages/web/CHANGELOG.md b/packages/web/CHANGELOG.md index 2fed37d611..ace9314fd7 100644 --- a/packages/web/CHANGELOG.md +++ b/packages/web/CHANGELOG.md @@ -8,6 +8,10 @@ All notable changes to this project will be documented in this file. +## 9.23.0 ((8/26/2026, 09:18 AM PST)) + +This is an artificial version bump with no new change. + ## 9.22.0 (8/25/2026 PST) #### 🚀 Updates diff --git a/packages/web/package.json b/packages/web/package.json index 619667eb27..48ee7ca7aa 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -1,6 +1,6 @@ { "name": "@coinbase/cds-web", - "version": "9.22.0", + "version": "9.23.0", "description": "Coinbase Design System - Web", "repository": { "type": "git",