Skip to content

Commit

Permalink
fix: Forward branding CSS variables through portals (#2482)
Browse files Browse the repository at this point in the history
Forwards the theme overrides to the stack element used in Popups and Modals. This fix also removes unnecessary style overrides for branding colors that aren't changed from the default.

Fixes: #2480

[category:Components]
  • Loading branch information
NicholasBoll authored Dec 19, 2023
1 parent 2936b49 commit 7747c61
Show file tree
Hide file tree
Showing 3 changed files with 118 additions and 16 deletions.
70 changes: 56 additions & 14 deletions modules/react/common/lib/CanvasProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,51 @@ import {defaultCanvasTheme, PartialEmotionCanvasTheme, useTheme} from './theming
import {brand} from '@workday/canvas-tokens-web';
// eslint-disable-next-line @emotion/no-vanilla
import {cache} from '@emotion/css';
import {createStyles} from '@workday/canvas-kit-styling';

export interface CanvasProviderProps {
theme?: PartialEmotionCanvasTheme;
}

// copied from brand/_variables.css
const defaultBranding = createStyles({
'--cnvs-brand-error-darkest': 'rgba(128,22,14,1)',
'--cnvs-brand-common-alert-inner': 'var(--cnvs-base-palette-cantaloupe-400)',
'--cnvs-brand-common-error-inner': 'var(--cnvs-base-palette-cinnamon-500)',
'--cnvs-brand-common-focus-outline': 'var(--cnvs-base-palette-blueberry-400)',
'--cnvs-brand-neutral-accent': 'var(--cnvs-base-palette-french-vanilla-100)',
'--cnvs-brand-neutral-darkest': 'var(--cnvs-base-palette-licorice-400)',
'--cnvs-brand-neutral-dark': 'var(--cnvs-base-palette-licorice-300)',
'--cnvs-brand-neutral-base': 'var(--cnvs-base-palette-soap-600)',
'--cnvs-brand-neutral-light': 'var(--cnvs-base-palette-soap-300)',
'--cnvs-brand-neutral-lightest': 'var(--cnvs-base-palette-soap-200)',
'--cnvs-brand-success-accent': 'var(--cnvs-base-palette-french-vanilla-100)',
'--cnvs-brand-success-darkest': 'var(--cnvs-base-palette-green-apple-600)',
'--cnvs-brand-success-dark': 'var(--cnvs-base-palette-green-apple-500)',
'--cnvs-brand-success-base': 'var(--cnvs-base-palette-green-apple-400)',
'--cnvs-brand-success-light': 'var(--cnvs-base-palette-green-apple-300)',
'--cnvs-brand-success-lightest': 'var(--cnvs-base-palette-green-apple-100)',
'--cnvs-brand-error-accent': 'var(--cnvs-base-palette-french-vanilla-100)',
'--cnvs-brand-error-dark': 'var(--cnvs-base-palette-cinnamon-600)',
'--cnvs-brand-error-base': 'var(--cnvs-base-palette-cinnamon-500)',
'--cnvs-brand-error-light': 'var(--cnvs-base-palette-cinnamon-200)',
'--cnvs-brand-error-lightest': 'var(--cnvs-base-palette-cinnamon-100)',
'--cnvs-brand-alert-accent': 'var(--cnvs-base-palette-french-vanilla-100)',
'--cnvs-brand-alert-darkest': 'var(--cnvs-base-palette-cantaloupe-600)',
'--cnvs-brand-alert-dark': 'var(--cnvs-base-palette-cantaloupe-500)',
'--cnvs-brand-alert-base': 'var(--cnvs-base-palette-cantaloupe-400)',
'--cnvs-brand-alert-light': 'var(--cnvs-base-palette-cantaloupe-200)',
'--cnvs-brand-alert-lightest': 'var(--cnvs-base-palette-cantaloupe-100)',
'--cnvs-brand-primary-accent': 'var(--cnvs-base-palette-french-vanilla-100)',
'--cnvs-brand-primary-darkest': 'var(--cnvs-base-palette-blueberry-600)',
'--cnvs-brand-primary-dark': 'var(--cnvs-base-palette-blueberry-500)',
'--cnvs-brand-primary-base': 'var(--cnvs-base-palette-blueberry-400)',
'--cnvs-brand-primary-light': 'var(--cnvs-base-palette-blueberry-200)',
'--cnvs-brand-primary-lightest': 'var(--cnvs-base-palette-blueberry-100)',
'--cnvs-brand-gradient-primary':
'linear-gradient(90deg, var(--cnvs-brand-primary-base) 0%, var(--cnvs-brand-primary-dark) 100%)',
});

const mappedKeys = {
lightest: 'lightest',
light: 'light',
Expand All @@ -19,27 +59,29 @@ const mappedKeys = {
contrast: 'accent',
};

const useCanvasThemeToCssVars = (
export const useCanvasThemeToCssVars = (
theme: PartialEmotionCanvasTheme | undefined,
elemProps: React.HTMLAttributes<HTMLElement>
) => {
const filledTheme = useTheme(theme);
const className = (elemProps.className || '').split(' ').concat(defaultBranding).join(' ');
const style = elemProps.style || {};
const {palette} = filledTheme.canvas;
const style = (['common', 'primary', 'error', 'alert', 'success', 'neutral'] as const).reduce(
(result, color) => {
if (color === 'common') {
(['common', 'primary', 'error', 'alert', 'success', 'neutral'] as const).forEach(color => {
if (color === 'common') {
// @ts-ignore
style[brand.common.focusOutline] = palette.common.focusOutline;
}
(['lightest', 'light', 'main', 'dark', 'darkest', 'contrast'] as const).forEach(key => {
// We only want to set custom colors if they do not match the default. The `defaultBranding` class will take care of the rest.
// @ts-ignore
if (palette[color][key] !== defaultCanvasTheme.palette[color][key]) {
// @ts-ignore
result[brand.common.focusOutline] = palette.common.focusOutline;
style[brand[color][mappedKeys[key]]] = palette[color][key];
}
(['lightest', 'light', 'main', 'dark', 'darkest', 'contrast'] as const).forEach(key => {
// @ts-ignore
result[brand[color][mappedKeys[key]]] = palette[color][key];
});
return result;
},
elemProps.style || {}
);
return {...elemProps, style};
});
});
return {...elemProps, className, style};
};

export const CanvasProvider = ({
Expand Down
30 changes: 29 additions & 1 deletion modules/react/modal/stories/stories_VisualTesting.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {Modal, useModalModel} from '@workday/canvas-kit-react/modal';
import {ContentDirection, CanvasProvider, useTheme} from '@workday/canvas-kit-react/common';
import {Flex, Box} from '@workday/canvas-kit-react/layout';

import {withSnapshotsEnabled} from '../../../../utils/storybook';
import {customColorTheme, withSnapshotsEnabled} from '../../../../utils/storybook';

const TestContent = () => {
const content = (
Expand Down Expand Up @@ -77,3 +77,31 @@ export const ModalRTL = withSnapshotsEnabled(() => {
</CanvasProvider>
);
});

export const CustomThemeModal = withSnapshotsEnabled(() => {
const model = useModalModel({
initialVisibility: 'visible',
});
return (
<CanvasProvider theme={{canvas: customColorTheme}}>
<Modal model={model}>
<Modal.Overlay style={{animation: 'none'}}>
<Modal.Card style={{animation: 'none'}}>
<Modal.CloseIcon aria-label="Close" />
<Modal.Heading>MIT License</Modal.Heading>
<Modal.Body>
<Box as="p" marginY="zero">
Permission is hereby granted, free of charge, to any person obtaining a copy of this
software and associated documentation files (the "Software").
</Box>
</Modal.Body>
<Flex gap="s" padding="xxs" marginTop="xxs">
<Modal.CloseButton as={PrimaryButton}>Acknowledge</Modal.CloseButton>
<Modal.CloseButton>Cancel</Modal.CloseButton>
</Flex>
</Modal.Card>
</Modal.Overlay>
</Modal>
</CanvasProvider>
);
});
34 changes: 33 additions & 1 deletion modules/react/popup/lib/hooks/usePopupStack.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import React from 'react';

import {PopupStack} from '@workday/canvas-kit-popup-stack';
import {useLocalRef, useIsRTL} from '@workday/canvas-kit-react/common';
import {useLocalRef, useIsRTL, useCanvasThemeToCssVars} from '@workday/canvas-kit-react/common';
import {ThemeContext, Theme} from '@emotion/react';

/**
* **Note:** If you're using {@link Popper}, you do not need to use this hook directly.
Expand Down Expand Up @@ -51,6 +52,8 @@ export const usePopupStack = <E extends HTMLElement>(
): React.RefObject<HTMLElement> => {
const {elementRef, localRef} = useLocalRef(ref);
const isRTL = useIsRTL();
const theme = React.useContext(ThemeContext as React.Context<Theme>);
const {className, style} = useCanvasThemeToCssVars(theme, {});

// useState function input ensures we only create a container once.
const [popupRef] = React.useState(() => {
Expand Down Expand Up @@ -90,5 +93,34 @@ export const usePopupStack = <E extends HTMLElement>(
}
}, [localRef, isRTL]);

// theming className
React.useLayoutEffect(() => {
const element = localRef.current;
element?.classList.add(className.trim());
return () => {
element?.classList.remove(className.trim());
};
}, [localRef, className]);

React.useLayoutEffect(() => {
const element = localRef.current;
if (element) {
// eslint-disable-next-line guard-for-in
for (const key in style) {
// @ts-ignore
element.style.setProperty(key, style[key]);
}
}
return () => {
if (element) {
// eslint-disable-next-line guard-for-in
for (const key in style) {
// @ts-ignore
element.style.removeProperty(key, style[key]);
}
}
};
}, [localRef, style]);

return localRef;
};

0 comments on commit 7747c61

Please sign in to comment.