fix: Add backwards compatability with canvas provider#3773
fix: Add backwards compatability with canvas provider#3773mannycarrera4 wants to merge 12 commits intoWorkday:prerelease/majorfrom
Conversation
Workday/canvas-kit
|
||||||||||||||||||||||||||||||||||||||||
| Project |
Workday/canvas-kit
|
| Branch Review |
mc-forward-theming
|
| Run status |
|
| Run duration | 02m 26s |
| Commit |
|
| Committer | Manuel Carrera |
| View all properties for this run ↗︎ | |
| Test results | |
|---|---|
|
|
0
|
|
|
0
|
|
|
15
|
|
|
0
|
|
|
790
|
| View all changes introduced in this branch ↗︎ | |
UI Coverage
19.45%
|
|
|---|---|
|
|
1522
|
|
|
365
|
Accessibility
99.38%
|
|
|---|---|
|
|
5 critical
5 serious
0 moderate
2 minor
|
|
|
72
|
…/canvas-kit into mc-forward-theming
There was a problem hiding this comment.
Pull request overview
This PR adds backwards compatibility to CanvasProvider for customizing theming via the theme prop. It addresses issue #3747 where the old theme format (e.g., brand.primary.base) doesn't work with the new semantic brand token system (e.g., brand.primary.600 and system.color.brand.*). The implementation creates mappings from deprecated theme palette keys to both legacy brand tokens and new numerical/system brand tokens.
Changes:
- Added comprehensive token mapping logic in
CanvasProviderto forward theme values from old palette format to new brand and system tokens - Updated popup theming to apply CSS variables earlier and remove cleanup to prevent theme flashing
- Updated documentation and examples to demonstrate scoped theming with the new backwards-compatible approach
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 16 comments.
Show a summary per file
| File | Description |
|---|---|
| modules/react/common/lib/CanvasProvider.tsx | Core backwards compatibility logic with mappings from deprecated palette keys to numerical brand tokens and system tokens |
| modules/react/popup/lib/hooks/usePopupStack.ts | Refactored theme application timing and removed cleanup to prevent cascade barriers and theme flashing |
| modules/react/popup/stories/visual-testing/Popup.stories.tsx | Updated PopupThemed story to properly test themed popup with trigger button |
| modules/react/common/stories/mdx/examples/Theming.tsx | Updated example to demonstrate scoped theming with CanvasProvider theme prop |
| modules/react/common/stories/mdx/Theming.mdx | Extensive documentation updates explaining v14/v15 theming approach and backwards compatibility |
| modules/docs/mdx/15.0-UPGRADE-GUIDE.mdx | Added section explaining system brand tokens and backwards compatibility |
| modules/docs/llm/upgrade-guides/15.0-UPGRADE-GUIDE.md | Added section explaining system brand tokens and backwards compatibility |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| const commonTokenMapping = { | ||
| focusOutline: brand.common.focus, // maps to brand.primary500 | ||
| alertInner: brand.common.caution.inner, // maps to brand.caution400 | ||
| alertOuter: brand.common.caution.outer, // maps to brand.caution500 |
There was a problem hiding this comment.
Inconsistent token naming format in comment. This should use dot notation "brand.caution.500" to match the format used in the documentation comments above.
| alertOuter: brand.common.caution.outer, // maps to brand.caution500 | |
| alertOuter: brand.common.caution.outer, // maps to brand.caution.500 |
| } else if (key === 'dark') { | ||
| // system.color.brand.fg.{color}.strong -> brand.{color}.700 | ||
| // @ts-ignore | ||
| const systemFgStrongToken = system.color.brand.fg[newBrandColor]?.strong; | ||
| if (systemFgStrongToken) { | ||
| // @ts-ignore | ||
| style[systemFgStrongToken] = value; | ||
| } | ||
|
|
||
| // system.color.brand.fg.selected -> brand.primary.700 (for primary only) | ||
| if (newBrandColor === 'primary') { | ||
| // @ts-ignore | ||
| const selectedToken = system.color.brand.fg.selected; | ||
| if (selectedToken) { | ||
| // @ts-ignore | ||
| style[selectedToken] = value; | ||
| } | ||
| } |
There was a problem hiding this comment.
Missing implementation for system token forwarding. According to the documentation at lines 65, 71-72, system.color.brand.focus.critical and system.color.brand.border.critical should be controlled by palette.error.dark, and system.color.brand.border.caution should be controlled by palette.alert.dark. However, in the implementation at lines 271-288, when key === 'dark', only system.color.brand.fg.{color}.strong and system.color.brand.fg.selected are forwarded. The focus and border tokens for critical and caution colors are not being set, which means theming palette.error.dark or palette.alert.dark won't affect these system tokens as documented.
| ## Global vs Scoped Theming | ||
|
|
||
| Canvas Kit v14 supports two theming strategies: **global theming** and **scoped theming**. Understanding the difference is important to avoid unexpected behavior. | ||
| Canvas Kit v14 and v15 supports two theming strategies: **global theming** and **scoped theming**. Understanding the difference is important to avoid unexpected behavior. |
There was a problem hiding this comment.
Subject-verb agreement error. "Canvas Kit v14 and v15" is plural, so it should use "support" (plural verb) instead of "supports" (singular verb).
| Canvas Kit v14 and v15 supports two theming strategies: **global theming** and **scoped theming**. Understanding the difference is important to avoid unexpected behavior. | |
| Canvas Kit v14 and v15 support two theming strategies: **global theming** and **scoped theming**. Understanding the difference is important to avoid unexpected behavior. |
| */ | ||
| const commonTokenMapping = { | ||
| focusOutline: brand.common.focus, // maps to brand.primary500 | ||
| alertInner: brand.common.caution.inner, // maps to brand.caution400 |
There was a problem hiding this comment.
Inconsistent token naming format in comment. Similar to line 104, this should use dot notation "brand.caution.400" to match the format used in the documentation comments above (lines 64-101).
| alertInner: brand.common.caution.inner, // maps to brand.caution400 | |
| alertInner: brand.common.caution.inner, // maps to brand.caution.400 |
|
|
||
| ### System Brand Tokens and Brand Tokens | ||
|
|
||
| The relationship between **system brand tokens** (e.g. `system.color.brand.accent.primary`) and **brand tokens** (e.g. `brand.primary600`) has changed. Teams can still set palette values such as `base`, `dark`, `darkest`, `light`, `lightest`, and `lighter` via the `CanvasProvider` theme prop. The mapping inside `CanvasProvider` exists for **backwards compatibility**: when you pass a theme object, we forward those values to both the legacy brand tokens and the system brand tokens so existing usage continues to work. |
There was a problem hiding this comment.
Inconsistent token naming format. The token is shown as "brand.primary600" but this is inconsistent with the documentation format used in CanvasProvider.tsx (lines 64-101) where tokens are shown with dots for readability (e.g., "brand.primary.600"). Consider using "brand.primary.600" for consistency with the rest of the documentation.
| The relationship between **system brand tokens** (e.g. `system.color.brand.accent.primary`) and **brand tokens** (e.g. `brand.primary600`) has changed. Teams can still set palette values such as `base`, `dark`, `darkest`, `light`, `lightest`, and `lighter` via the `CanvasProvider` theme prop. The mapping inside `CanvasProvider` exists for **backwards compatibility**: when you pass a theme object, we forward those values to both the legacy brand tokens and the system brand tokens so existing usage continues to work. | |
| The relationship between **system brand tokens** (e.g. `system.color.brand.accent.primary`) and **brand tokens** (e.g. `brand.primary.600`) has changed. Teams can still set palette values such as `base`, `dark`, `darkest`, `light`, `lightest`, and `lighter` via the `CanvasProvider` theme prop. The mapping inside `CanvasProvider` exists for **backwards compatibility**: when you pass a theme object, we forward those values to both the legacy brand tokens and the system brand tokens so existing usage continues to work. |
| // system.color.brand.focus.primary (maps to brand.primary.500 per docs) | ||
| // For primary only, update focus border when 'main' changes | ||
| if (newBrandColor === 'primary') { | ||
| // Calculate a reasonable focus color based on the main color | ||
| // We'll use the main value since brand.primary.500 derives from it | ||
| // @ts-ignore | ||
| const focusToken = system.color.brand.focus.primary; | ||
| if (focusToken) { | ||
| // @ts-ignore | ||
| style[focusToken] = value; | ||
| } | ||
| } |
There was a problem hiding this comment.
Potential token override conflict. The code sets system.color.brand.focus.primary in two places: when palette.common.focusOutline is customized (lines 184-191) and when palette.primary.main is customized (lines 259-270). Since the color iteration order is 'common' first then 'primary' (line 169), if a user sets both palette.common.focusOutline and palette.primary.main, the palette.primary.main value will override palette.common.focusOutline for system.color.brand.focus.primary. This precedence should be either documented in the comment at line 64, or the code should check if focusOutline was already customized before overriding it with palette.primary.main.
| focusOutline: brand.common.focus, // maps to brand.primary500 | ||
| alertInner: brand.common.caution.inner, // maps to brand.caution400 | ||
| alertOuter: brand.common.caution.outer, // maps to brand.caution500 | ||
| errorInner: brand.common.critical, // maps to brand.critical500 |
There was a problem hiding this comment.
Inconsistent token naming format in comment. This should use dot notation "brand.critical.500" to match the format used in the documentation comments above.
| focusOutline: brand.common.focus, // maps to brand.primary500 | |
| alertInner: brand.common.caution.inner, // maps to brand.caution400 | |
| alertOuter: brand.common.caution.outer, // maps to brand.caution500 | |
| errorInner: brand.common.critical, // maps to brand.critical500 | |
| focusOutline: brand.common.focus, // maps to brand.primary.500 | |
| alertInner: brand.common.caution.inner, // maps to brand.caution.400 | |
| alertOuter: brand.common.caution.outer, // maps to brand.caution.500 | |
| errorInner: brand.common.critical, // maps to brand.critical.500 |
| * - `system.color.brand.focus.primary` → `brand.primary.500` → Controlled by `palette.primary.main` or `focusOutline` | ||
| * - `system.color.brand.focus.critical` → `brand.critical.500` → Controlled by `palette.error.dark` or `errorInner` | ||
| * - `system.color.brand.focus.caution.inner` → `brand.caution.400` → Controlled by `palette.alert.main` or `alertInner` | ||
| * - `system.color.brand.focus.caution.outer` → `brand.caution.500` → Controlled by `palette.alert.dark` or `alertOuter` | ||
| * | ||
| * ### Border Tokens | ||
| * - `system.color.brand.border.primary` → `brand.primary.500` → Controlled by `palette.primary.main` or `focusOutline` |
There was a problem hiding this comment.
Incorrect token mapping in documentation. The comment states that palette.primary.main maps to brand.primary.500, but according to the numericalTokenMapping at line 38, main maps to '600' which would produce brand.primary.600, not brand.primary.500. The comment should indicate that focusOutline controls brand.primary.500 separately from palette.primary.main which controls brand.primary.600.
| * - `system.color.brand.focus.primary` → `brand.primary.500` → Controlled by `palette.primary.main` or `focusOutline` | |
| * - `system.color.brand.focus.critical` → `brand.critical.500` → Controlled by `palette.error.dark` or `errorInner` | |
| * - `system.color.brand.focus.caution.inner` → `brand.caution.400` → Controlled by `palette.alert.main` or `alertInner` | |
| * - `system.color.brand.focus.caution.outer` → `brand.caution.500` → Controlled by `palette.alert.dark` or `alertOuter` | |
| * | |
| * ### Border Tokens | |
| * - `system.color.brand.border.primary` → `brand.primary.500` → Controlled by `palette.primary.main` or `focusOutline` | |
| * - `system.color.brand.focus.primary` → `brand.primary.500` → Controlled by `focusOutline` (separately from `palette.primary.main`, which controls `brand.primary.600`) | |
| * - `system.color.brand.focus.critical` → `brand.critical.500` → Controlled by `palette.error.dark` or `errorInner` | |
| * - `system.color.brand.focus.caution.inner` → `brand.caution.400` → Controlled by `palette.alert.main` or `alertInner` | |
| * - `system.color.brand.focus.caution.outer` → `brand.caution.500` → Controlled by `palette.alert.dark` or `alertOuter` | |
| * | |
| * ### Border Tokens | |
| * - `system.color.brand.border.primary` → `brand.primary.500` → Controlled by `focusOutline` (separately from `palette.primary.main`, which controls `brand.primary.600`) |
| ## Overview | ||
|
|
||
| Canvas Kit v14 introduces a significant shift in our approach to theming: we've moved away from | ||
| Canvas Kit v14 and v15 introduces a significant shift in our approach to theming: we've moved away from |
There was a problem hiding this comment.
Subject-verb agreement error. "Canvas Kit v14 and v15" is plural, so it should use "introduce" (plural verb) instead of "introduces" (singular verb).
| Canvas Kit v14 and v15 introduces a significant shift in our approach to theming: we've moved away from | |
| Canvas Kit v14 and v15 introduce a significant shift in our approach to theming: we've moved away from |
| // Forward to all relevant system.color.brand.* tokens | ||
| // These system tokens reference the numerical brand tokens, so updating them ensures full compatibility | ||
| if (key === 'main') { | ||
| // system.color.brand.accent.{color} -> brand.{color}.600 (except caution -> 400) |
There was a problem hiding this comment.
Comment is potentially misleading. The comment says "except caution -> 400" suggesting caution is an exception, but based on the documentation at line 88, system.color.brand.accent.caution does map to brand.caution.400, not brand.caution.600. The code handles this correctly by forwarding palette.alert.main to the accent token, but the comment should clarify that for caution/alert, the mapping is intentionally to .400 instead of .600, not that it's an exception that needs special handling in the code.
| // system.color.brand.accent.{color} -> brand.{color}.600 (except caution -> 400) | |
| // system.color.brand.accent.{color} -> corresponding brand.{color} token | |
| // (per docs, most map to .600; caution/alert intentionally maps to .400 via the token definition) |
| /** | ||
| * Mapping from deprecated common palette keys to new brand.common tokens. | ||
| * | ||
| * ## Brandable System Tokens |
There was a problem hiding this comment.
Are we keeping all of these comments?
Summary
Fixes: #3747
Release Category
Components
Release Note
We've added backwards compatibility to
CanvasProviderto customize theming via thethemeprop even though our components now use semantic brandable tokens. Consumers should only use this approach when creating ascopedtheme intended to break away from global theming.Checklist
ready for reviewhas been added to PRFor the Reviewer
Where Should the Reviewer Start?
Areas for Feedback? (optional)
Testing Manually
Screenshots or GIFs (if applicable)
Thank You Gif (optional)