diff --git a/modules/react/common/lib/CanvasProvider.tsx b/modules/react/common/lib/CanvasProvider.tsx index 3c71f37195..424ac3ab4b 100644 --- a/modules/react/common/lib/CanvasProvider.tsx +++ b/modules/react/common/lib/CanvasProvider.tsx @@ -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', @@ -19,27 +59,29 @@ const mappedKeys = { contrast: 'accent', }; -const useCanvasThemeToCssVars = ( +export const useCanvasThemeToCssVars = ( theme: PartialEmotionCanvasTheme | undefined, elemProps: React.HTMLAttributes ) => { 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 = ({ diff --git a/modules/react/modal/stories/stories_VisualTesting.tsx b/modules/react/modal/stories/stories_VisualTesting.tsx index b04257aca0..57bb24df11 100644 --- a/modules/react/modal/stories/stories_VisualTesting.tsx +++ b/modules/react/modal/stories/stories_VisualTesting.tsx @@ -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 = ( @@ -77,3 +77,31 @@ export const ModalRTL = withSnapshotsEnabled(() => { ); }); + +export const CustomThemeModal = withSnapshotsEnabled(() => { + const model = useModalModel({ + initialVisibility: 'visible', + }); + return ( + + + + + + MIT License + + + Permission is hereby granted, free of charge, to any person obtaining a copy of this + software and associated documentation files (the "Software"). + + + + Acknowledge + Cancel + + + + + + ); +}); diff --git a/modules/react/popup/lib/hooks/usePopupStack.ts b/modules/react/popup/lib/hooks/usePopupStack.ts index cd083e4dd3..788baa34d3 100644 --- a/modules/react/popup/lib/hooks/usePopupStack.ts +++ b/modules/react/popup/lib/hooks/usePopupStack.ts @@ -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. @@ -51,6 +52,8 @@ export const usePopupStack = ( ): React.RefObject => { const {elementRef, localRef} = useLocalRef(ref); const isRTL = useIsRTL(); + const theme = React.useContext(ThemeContext as React.Context); + const {className, style} = useCanvasThemeToCssVars(theme, {}); // useState function input ensures we only create a container once. const [popupRef] = React.useState(() => { @@ -90,5 +93,34 @@ export const usePopupStack = ( } }, [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; };