feat: add createIcon factory to decouple Icon from a single icon set#786
feat: add createIcon factory to decouple Icon from a single icon set#786adrienzheng-cb wants to merge 7 commits into
Conversation
🟡 Heimdall Review Status
🟡
|
| Code Owner | Status | Calculation | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
| ui-systems-eng-team |
🟡
0/1
|
Denominator calculation
|
3ceafc6 to
0d6d6a6
Compare
|
|
||
| ### Custom icon sets | ||
|
|
||
| CDS ships a single default icon font (`@coinbase/cds-icons`). If you maintain your own icon package — with its own generated glyph map, font, and name union — use `createIcon` to build a fully-typed icon component bound to that set. It reuses all of the default `Icon`'s rendering, sizing, color, and accessibility behavior; only the glyphs, font, and `name` type change. In fact, the default `Icon` is itself `createIcon<IconName>({ glyphMap, fontFamily: 'CoinbaseIcons' })`, so this API is fully backward compatible. |
There was a problem hiding this comment.
with its own generated glyph map, font, and name union
This comes across as kind of confusing without knowing what any of them are.
I wonder if it would just be better to not document createIcon until we have a better way to document all the required arguments and make them a little more self-service? Otherwise i think we need to simplify by taking out some technical details and simply showing how createIcon is used. We could say in the future it may be possible to build your own icon set
There was a problem hiding this comment.
maybe there is a helpful doc about glyphmap icons to link to for supplemental reading
| * icon component that reuses all of the CDS rendering, accessibility, and | ||
| * theming behavior. | ||
| */ | ||
| export function createIcon<Name extends string>({ |
There was a problem hiding this comment.
need unit tests like web
| const rootStyle = useMemo( | ||
| () => [ | ||
| { | ||
| paddingTop: theme.space[paddingTop ?? paddingY ?? padding ?? 0], |
There was a problem hiding this comment.
this is really interesting... i wonder what other _props should actually be going to the Box contianer and not the inner Text. I see we are not spreading anywhere so hopefully the set is small
There was a problem hiding this comment.
This is intentional — mobile createIcon destructures a fixed, closed set of props and never spreads ...props onto the Box or the inner Text, so nothing leaks through. Anything not in that set is simply not accepted. If we later want web-style Box passthrough on mobile, that'd be a separate, deliberate change.
| const sourceSize = getSourceSize(iconSize); | ||
|
|
||
| const iconColor = theme.color[color]; | ||
| const finalColor = dangerouslySetColor ?? iconColor; |
There was a problem hiding this comment.
nit: make sure this prop has a deprecation date
There was a problem hiding this comment.
if so, we could probably consider dropping it since the color prop covers this and styles.icon.color should work as well
There was a problem hiding this comment.
dropping as in the business logic, keeping the deprecated prop for bw compat but have it wired up to nothgin
There was a problem hiding this comment.
Good call. It already has @deprecationExpectedRemoval v10. I'd prefer to keep the current wiring for now rather than turn it into a no-op early, since that's a behavior change for existing consumers still passing dangerouslySetColor. Plan is to drop the business logic (and/or the prop) as part of the v10 removal, where color and styles.icon.color fully cover it.
| /** | ||
| * `createIcon` bound to a different font (Google Material Icons), showing a | ||
| * consumer can supply their own glyph map, font, and name union while reusing | ||
| * the CDS Icon renderer. Uses a network font, so percy is skipped. |
There was a problem hiding this comment.
what do you mean percy is skipped?
There was a problem hiding this comment.
we don't include this story in visreg, because the font comes from a external source and may change and break the test.
There was a problem hiding this comment.
I'm gonna remove this comment tho, it is too trivial a detail.
Extract the web and mobile Icon rendering into a `createIcon<Name>` factory
that binds a glyph map, font family, and typed name union. The default
`Icon` becomes `createIcon<IconName>({ glyphMap, fontFamily })`, so behavior
and the public API are unchanged, while consumers with their own icon
package can create a typed icon component that reuses all CDS rendering,
accessibility, and theming logic.
Font family is now injectable (moved off the static Linaria block on web to
an inline style) rather than hardcoded, enabling alternate icon sets without
forking the component.
Co-authored-by: Cursor <cursoragent@cursor.com>
Keep the static Linaria block and express font-family as `var(--cds-icon-font-family, 'CoinbaseIcons')`. The factory only sets the variable when a non-default font is bound, so consumers can override the icon font via className, inline style, or theme scope without a specificity battle. Default rendering is unchanged. Co-authored-by: Cursor <cursoragent@cursor.com>
Unit tests cover the seam: rendering a glyph from a supplied map, setting the --cds-icon-font-family CSS variable only for a custom font, and falling back when a glyph is missing. The Storybook entry binds Google Material Icons through createIcon to visually demonstrate a second, typed icon set running on a different font (percy-skipped, network font). Co-authored-by: Cursor <cursoragent@cursor.com>
Adds a "Custom icon sets" section to the web and mobile Icon docs covering the createIcon factory, glyph map contract, and font loading, plus an editable live demo on web. Also tightens a couple of verbose comments in the web factory and custom-font story. Co-authored-by: Cursor <cursoragent@cursor.com>
Replaces the `getSourceSize` config (which only remapped the pixel size)
with a more general `getGlyph` resolver that controls how a glyph is looked
up from the map. The default resolver keeps the CDS
`${name}-${sourceSize}-${state}` scheme, so behavior is unchanged, but
consumers with a different key format or size model can now override the
whole lookup. Exports the new `IconGlyphResolverArgs` type and documents
the override on the Icon docs page.
Co-authored-by: Cursor <cursoragent@cursor.com>
Switches the custom-font Storybook demo from a story-level `percy: { skip }`
parameter to the centralized `.percy.js` exclude list, matching how other
non-deterministic (animation/network) stories are handled.
Co-authored-by: Cursor <cursoragent@cursor.com>
74a94a2 to
b24e5b9
Compare
Address PR review feedback: add mobile createIcon unit tests mirroring web (plus getGlyph resolver coverage), simplify the public createIcon docs to reduce jargon, and drop a trivial Percy comment from the story. Co-authored-by: Cursor <cursoragent@cursor.com>
What changed? Why?
The web and mobile
Iconcomponents hardcode a single icon source (@coinbase/cds-icons/glyphMap), a single font family ('CoinbaseIcons'), and a singlenametype (IconName). That makes it impossible to render an alternate icon set (e.g. a private, retail-specific style) without forking the whole component.This PR extracts the rendering logic into a
createIcon<Name>factory (mirroring the existingcreateIllustration<Variant>convention) that binds:GlyphMap<Name>, keys`${name}-${size}-${active}`),'CoinbaseIcons'),Name), andgetGlyph) for sets with a different key format or size model.The default
Iconbecomes a one-liner:So the public API and behavior are unchanged, while a consumer with their own icon package can do
createIcon<RetailIconName>({ glyphMap, fontFamily })and get a fully-typed icon component that reuses all CDS accessibility, theming, sizing, andcomponentConfigbehavior — no duplicated component logic.Glyph resolution
Rather than a narrow
getSourceSizehook that only remapped the pixel size, the factory accepts a generalgetGlyphresolver:(args: IconGlyphResolverArgs<Name>) => string | undefined, where args carryglyphMap,name,sizetoken, resolvedpixelSize, andactive. The default resolver keeps the CDS`${name}-${sourceSize}-${'active' | 'inactive'}`scheme (sourceSize= 12/16/24), so nothing changes for the defaultIcon; consumers whose maps are keyed differently can override the entire lookup.Font family (web)
On web,
font-familyis applied through a--cds-icon-font-familyCSS custom property (default'CoinbaseIcons', declared in the static Linaria block).createIcononly sets the variable inline when a non-default font is bound, so the default keeps its fully static CSS while consumers can override the font per instance viaclassName/stylesor by scoping the variable on an ancestor. Mobile appliesfontFamilyvia its inline style object (React Native has no CSS variables).Docs
The Icon docs page gains a "Custom icon sets" section (web + mobile) documenting the factory, the glyph map contract, the
getGlyphoverride, and font loading. The web page includes an editable live demo that bindscreateIconto Google Material Icons to prove a second, fully-typed icon set works end-to-end.Root cause (required for bugfixes)
N/A — additive infrastructure.
UI changes
None. The default icon set, font, glyph keys, and sizing are identical, so rendering is unchanged.
Testing
How has it been tested?
Testing instructions
yarn nx run-many --target=typecheck --projects=web,mobile— passyarn nx run web:test --testPathPattern="createIcon.test"— pass (4 tests: renders glyph, sets/omits the font-family CSS variable, renders fallback)yarn nx run mobile:test --testPathPattern="icons/"— passyarn nx run docs:start— docs build succeeds and the Icon page renders the live demoIllustrations/Icons Checklist
N/A — no files under
packages/icons/**orpackages/illustrations/**changed; no icon assets or names added.Change management
type=routine
risk=low
impact=sev5
automerge=false
Made with Cursor