diff --git a/modules/docs/mdx/9.0-UPGRADE-GUIDE.mdx b/modules/docs/mdx/9.0-UPGRADE-GUIDE.mdx index e45d551597..5d99526b89 100644 --- a/modules/docs/mdx/9.0-UPGRADE-GUIDE.mdx +++ b/modules/docs/mdx/9.0-UPGRADE-GUIDE.mdx @@ -14,14 +14,15 @@ any questions. - [Layout and Column](#layout-and-column) - [Stack](#stack-hstack-vstack) - [Component Updates](#component-updates) - - [Buttons](#buttons) - - [Toast](#toast) + - [Buttons](#buttons) - [Toast](#toast) - [Utility Updates](#utility-updates) - [focusRing](#focusring) - - [useThemedRing](#useThemedRing) - - [useThemeRTL](#useThemeRTL) + - [useTheme and getTheme](#useTheme-and-getTheme) + - [useCanvasTheme and getCanvasTheme](#useCanvasTheme-and-getCanvasTheme) + - [useThemedRing](#useThemedRing) + - [useThemeRTL](#useThemeRTL) - [Token Updates](#token-updates) - - [Depth](#depth) + - [Depth](#depth) ## Codemod @@ -175,6 +176,17 @@ We're removing memoization from focus ring. The `memoize` argument passed to `fo longer valid and we've removed the exported `memoizedFocusRing`. There is no codemod for this change. We couldn't find any example of `memoize` or `memoizedFocusRing` being used. +### useTheme and getTheme + +We've updated `useTheme` by adding error handling if this hook has been used outside a functional +component. Also, `getTheme` method has been added to access a theme from `styled` or class +components instead of `useTheme`. + +### useCanvasTheme and getCanvasTheme + +`useCanvasTheme` and `getCanvasTheme` have been removed and can be safely replaced by `useTheme` +and `getTheme`. + ### useThemedRing We've promoted `useThemedRing` from our Labs package to our Main package. You can use this utility @@ -184,7 +196,8 @@ to theme focus rings. ### useThemeRTL -We've [soft-deprecated](#soft-deprecation) `useThemeRTL` from our Labs package. Although you may still use this utility, we encourage consumers to use +We've [soft-deprecated](#soft-deprecation) `useThemeRTL` from our Labs package. Although you may +still use this utility, we encourage consumers to use [CSS logical properties](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Logical_Properties). ## Token Updates diff --git a/modules/react/action-bar/lib/ActionBarList.tsx b/modules/react/action-bar/lib/ActionBarList.tsx index 4063612f79..a99f420561 100644 --- a/modules/react/action-bar/lib/ActionBarList.tsx +++ b/modules/react/action-bar/lib/ActionBarList.tsx @@ -4,7 +4,7 @@ import {commonColors, colors, space} from '@workday/canvas-kit-react/tokens'; import { createSubcomponent, ExtractProps, - getCanvasTheme, + getTheme, styled, StyledType, } from '@workday/canvas-kit-react/common'; @@ -37,7 +37,7 @@ export interface ActionBarListProps } const ResponsiveList = styled(Flex)(({theme}) => { - const canvasTheme = getCanvasTheme(theme); + const {canvas: canvasTheme} = getTheme(theme); return { [canvasTheme.breakpoints.down('s')]: { padding: space.s, diff --git a/modules/react/common/lib/theming/README.md b/modules/react/common/lib/theming/README.md index 4d4c8d81e0..c28af882ce 100644 --- a/modules/react/common/lib/theming/README.md +++ b/modules/react/common/lib/theming/README.md @@ -241,6 +241,103 @@ window.workday = { If the theme is not available via a context, Canvas Kit components will attempt to pull it from this variable before falling back to the default theme. +## Accessing the theme value + +Canvas Kit provides two functions to access the current theme, `getTheme` and `useTheme`. If you +need to access the theme within a function component, use the hook, `useTheme`. If you need to +access the theme within a `styled` component, a class component, or outside a component, use +`getTheme`. The main difference is `useTheme` is intended to work within the `CanvasProvider` and +will check the `ThemeContext` for the theme first. Both functions check the window for the theme and +fall back to the default theme is nothing is found. Both functions will return a full theme object +in this shape: + +**Return value** + +```tsx +{ + canvas: { + palette: { + // ... + }, + breakpoints: { + // ... + }, + direction: ContentDirection.LTR, + }, + // other themes can be placed here +} +``` + +### getTheme + +`getTheme` is a function that returns the full theme object with the Canvas Kit theme under the +`canvas` key. It should be used with `styled` components, class components, or outside components. + +Below is an example of how to use `getTheme` to build responsive media query styles with the +breakpoint functions provided in the theme. + +```tsx +import {getTheme} from '@workday/canvas-kit-react/common'; + +const theme = getTheme(); +const {up, down} = theme.canvas.breakpoints; +const small = down('m'); // Returns '@media (max-width: 768px)' +const medium = up('m'); // Returns '@media (min-width: 768px)' +const styles = { + [small]: { + margin: space.m, + }, + [medium]: { + margin: space.l, + }, +}; +``` + +### useTheme + +`useTheme` is hook to get the full theme object. It should be used only with functional compoents +wrapped in ContextProvider. Function returns a theme object with the Canvas Kit theme under the +canvas key. + +`useTheme` should be used only inside functional component otherwise it will show a warning if the +theme context value has not been found. In that case you will need to use `getTheme`. + +Below is an example showing how to use `useTheme` in a function component to set `Subtext`'s color +to the error color provided by the theme. + +```tsx +export const ErrorMessage = () => { + const theme = useTheme(); + return ( + + ) +} +``` + +### Overwriting the theme + +You can also use both functions to overwrite the theme object by providing a partial or full theme +object to overwrite the current theme. In the example below, we're setting a custom content +direction, which can be passed to either `useTheme` or `getTheme`. These functions will properly +merge your the partial theme with the default Canvas theme and return a complete theme object. + +```tsx +import {ContentDirection, useTheme, getTheme} from '@workday/canvas-kit-react/common'; + +const customTheme = { + canvas: { + // set the content direction to right-to-left + direction: ContentDirection.RTL, + }, +}; + +// Overwriting the theme with useTheme +const customTheme = useTheme(customTheme); + +// Overwriting the theme with getTheme +const customTheme = getTheme(customTheme); +``` + ## Breakpoints Breakpoints are used by media queries to conditionally apply or modify styles based on the viewport diff --git a/modules/react/common/lib/theming/getCanvasTheme.ts b/modules/react/common/lib/theming/getCanvasTheme.ts deleted file mode 100644 index 574a18de75..0000000000 --- a/modules/react/common/lib/theming/getCanvasTheme.ts +++ /dev/null @@ -1,40 +0,0 @@ -import {CanvasTheme} from '@workday/canvas-kit-react/common'; -import {Theme, useTheme} from '@emotion/react'; -import {getObjectProxy} from './getObjectProxy'; -import {defaultCanvasTheme} from './theme'; - -/** - * Use this function when you have access to `theme` in styled components. - * This function ensures that a theme is defined via a proxy and allows access to properties on the theme object. - * @param theme Any partial or full theme object. - * @returns a theme object if it is defined, otherwise it will fall back on using defaultCanvasTheme - * @example - * const ResponsiveContainer = styled('div')(({theme}) => { - * const canvas = getCanvasTheme(theme); - * return { - * maxHeight: '100vh', - * display: 'flex', - * position: 'absolute', - * left: 0, - * top: 0, - * justifyContent: 'center', - * alignItems: 'center', - * height: '100vh', - * [canvas.breakpoints.down('s')]: { - * alignItems: 'end', - * }, - * }; - *}); - */ -export function getCanvasTheme(theme: any = {}): CanvasTheme { - // prettier-ignore - //@ts-ignore - const windowTheme = typeof window !== 'undefined' && window.workday && window.workday.canvas && window.workday.canvas.theme; - return getObjectProxy(theme.canvas || windowTheme || {}, defaultCanvasTheme); -} - -export function useCanvasTheme(themeOverride?: Theme) { - const theme = useTheme(); - - return getCanvasTheme(themeOverride || theme); -} diff --git a/modules/react/common/lib/theming/index.ts b/modules/react/common/lib/theming/index.ts index 273094ebd7..270b2a1f58 100644 --- a/modules/react/common/lib/theming/index.ts +++ b/modules/react/common/lib/theming/index.ts @@ -6,5 +6,4 @@ export * from './theme'; export * from './useTheme'; export * from './useThemedRing'; export * from './useIsRTL'; -export * from './getCanvasTheme'; export * from './getObjectProxy'; diff --git a/modules/react/common/lib/theming/useTheme.ts b/modules/react/common/lib/theming/useTheme.ts index 6a73e435cb..732bff5299 100644 --- a/modules/react/common/lib/theming/useTheme.ts +++ b/modules/react/common/lib/theming/useTheme.ts @@ -1,3 +1,6 @@ +// refactor for v5 +/// + import {useTheme as useEmotionTheme} from '@emotion/react'; import { defaultCanvasTheme, @@ -12,12 +15,61 @@ const getFilledTheme = (theme: PartialEmotionCanvasTheme) => ({ }); /** - * Hook function to get the correct theme object. - * @param {Object=} theme - The theme object returned from the emotion ThemeContext - * (through ThemeProvider). The Canvas Kit theme is namespaced within this variable under the `canvas` key. + * Function to get the correct theme object for `styled` and class components + * or to be used outside component. + * @param {Object=} theme - The theme object with the Canvas Kit theme. + * It should be namespaced within this variable under the `canvas` key. + * Value of `canvas` property is any partial or full theme object to overwtite default theme. + * + * @returns An object containing updated or default Canvas Kit theme under `canvas` key. + * + * The passed partial theme object will be merged with the default Canvas theme + * (using memoized createCanvasTheme()) to establish any missing fields that have + * not been defined by the consumer's theme object. + * + * If theme is not passed, the function will try to retrieve it from the window object. + * If window theme is not found, it will return the default Canvas theme. + * + * @example + * import {getTheme} from '@workday/canvas-kit-react/common'; + * + * const theme = getTheme(); + * const {up, down} = theme.canvas.breakpoints; + * const small = down('m'); // Returns '@media (max-width: 768px)' + * const medium = up('m'); // Returns '@media (min-width: 768px)' + * + * const styles = { + * [small]: { + * margin: space.m + * }, + * [medium]: { + * margin: space.l + * } + * } + */ +export function getTheme(theme?: PartialEmotionCanvasTheme): EmotionCanvasTheme { + if (theme?.canvas) { + return getFilledTheme(theme); + } + + const windowTheme = typeof window !== 'undefined' && (window as any)?.workday?.canvas?.theme; + + if (windowTheme) { + return getFilledTheme({canvas: windowTheme}); + } + + return {canvas: defaultCanvasTheme}; +} + +/** + * Hook function to get the correct theme object for functional components. + * @param {Object=} theme - The theme object with the Canvas Kit theme. + * It should be namespaced within this variable under the `canvas` key. + * Value of `canvas` property is any partial or full theme object to overwtite default theme. + * + * @returns An object containing updated or default Canvas Kit theme under `canvas` key. * - * NOTE: If you are using a class component, you MUST pass the theme. - * If not passed, the function will try to pull the theme from ThemeContext. + * NOTE: If theme is not passed, the function will try to pull the theme from ThemeContext. * If that does not work, it will try to retrieve it from the window object. * As a last resort, it will return the default Canvas theme. * @@ -28,26 +80,31 @@ const getFilledTheme = (theme: PartialEmotionCanvasTheme) => ({ * Providing the default theme here is currently a work around for when no * ThemeProvider or context exists. * Tracked on https://github.com/emotion-js/emotion/issues/1193. + * + * @example + * export const ErrorMessage = () => { + * const theme = useTheme(); + * return ( + * + * ); + * } */ export function useTheme(theme?: PartialEmotionCanvasTheme): EmotionCanvasTheme { - if (theme && theme.canvas) { - return getFilledTheme(theme); - } - - try { - // eslint-disable-next-line react-hooks/rules-of-hooks - const contextTheme = useEmotionTheme() as EmotionCanvasTheme; - if (contextTheme && contextTheme.canvas) { - return getFilledTheme(contextTheme); + if (!theme) { + try { + // eslint-disable-next-line react-hooks/rules-of-hooks + const contextTheme = useEmotionTheme() as EmotionCanvasTheme; + if (contextTheme?.canvas) { + return getFilledTheme(contextTheme); + } + } catch (e) { + if (process && process.env.NODE_ENV === 'development') { + console.warn( + 'useTheme: Context not supported or invalid. Please consider using `getTheme` function instead for `styled` or class components.' + ); + } } - } catch (e) { - // Context not supported or invalid (probably called from within a class component) - } - - const windowTheme = typeof window !== 'undefined' && (window as any)?.workday?.canvas?.theme; - if (windowTheme) { - return getFilledTheme({canvas: windowTheme}); } - return {canvas: defaultCanvasTheme}; + return getTheme(theme); } diff --git a/modules/react/common/stories/examples/ResponsiveViewport.tsx b/modules/react/common/stories/examples/ResponsiveViewport.tsx index f05d509c34..c24b948275 100644 --- a/modules/react/common/stories/examples/ResponsiveViewport.tsx +++ b/modules/react/common/stories/examples/ResponsiveViewport.tsx @@ -2,10 +2,9 @@ import * as React from 'react'; import {Box, Grid} from '@workday/canvas-kit-react/layout'; import styled from '@emotion/styled'; import {type, space, colors, borderRadius} from '@workday/canvas-kit-react/tokens'; -import {useTheme} from '@workday/canvas-kit-react/common'; +import {getTheme} from '@workday/canvas-kit-react/common'; -// eslint-disable-next-line react-hooks/rules-of-hooks -const theme = useTheme(); +const theme = getTheme(); const {up, down} = theme.canvas.breakpoints; const small = down('m'); // Returns '@media (max-width: 768px)' const medium = up('m'); // Returns '@media (min-width: 768px)' diff --git a/modules/react/modal/lib/ModalBody.tsx b/modules/react/modal/lib/ModalBody.tsx index 9988c95620..c59f84db99 100644 --- a/modules/react/modal/lib/ModalBody.tsx +++ b/modules/react/modal/lib/ModalBody.tsx @@ -3,7 +3,7 @@ import * as React from 'react'; import { createSubcomponent, ExtractProps, - getCanvasTheme, + getTheme, styled, StyledType, } from '@workday/canvas-kit-react/common'; @@ -15,7 +15,7 @@ import {useModalModel} from './hooks'; export interface ModalBodyProps extends ExtractProps {} const ResponsiveModalBody = styled(Popup.Body)(({theme}) => { - const canvasTheme = getCanvasTheme(theme); + const {canvas: canvasTheme} = getTheme(theme); return { [canvasTheme.breakpoints.down('s')]: { marginBottom: space.zero, diff --git a/modules/react/modal/lib/ModalCard.tsx b/modules/react/modal/lib/ModalCard.tsx index 1ced647b10..6d3da34097 100644 --- a/modules/react/modal/lib/ModalCard.tsx +++ b/modules/react/modal/lib/ModalCard.tsx @@ -5,7 +5,7 @@ import { ExtractProps, StyledType, styled, - getCanvasTheme, + getTheme, } from '@workday/canvas-kit-react/common'; import {space} from '@workday/canvas-kit-react/tokens'; import {Popup} from '@workday/canvas-kit-react/popup'; @@ -15,7 +15,7 @@ import {useModalCard, useModalModel} from './hooks'; export interface ModalCardProps extends ExtractProps {} const ResponsiveModalCard = styled(Popup.Card)(({theme}) => { - const canvasTheme = getCanvasTheme(theme); + const {canvas: canvasTheme} = getTheme(theme); return { margin: space.xl, [canvasTheme.breakpoints.down('s')]: { diff --git a/modules/react/modal/lib/ModalHeading.tsx b/modules/react/modal/lib/ModalHeading.tsx index cc0bc87588..8db683128e 100644 --- a/modules/react/modal/lib/ModalHeading.tsx +++ b/modules/react/modal/lib/ModalHeading.tsx @@ -3,7 +3,7 @@ import * as React from 'react'; import { createSubcomponent, ExtractProps, - getCanvasTheme, + getTheme, styled, StyledType, } from '@workday/canvas-kit-react/common'; @@ -15,7 +15,7 @@ import {useModalHeading, useModalModel} from './hooks'; export interface ModalHeadingProps extends ExtractProps {} const ResponsiveModalHeading = styled(Popup.Heading)(({theme}) => { - const canvasTheme = getCanvasTheme(theme); + const {canvas: canvasTheme} = getTheme(theme); return { [canvasTheme.breakpoints.down('s')]: { marginBottom: space.zero, diff --git a/modules/react/modal/lib/ModalOverlay.tsx b/modules/react/modal/lib/ModalOverlay.tsx index 0e780f13d8..cc29b00594 100644 --- a/modules/react/modal/lib/ModalOverlay.tsx +++ b/modules/react/modal/lib/ModalOverlay.tsx @@ -7,7 +7,7 @@ import { StyledType, useWindowSize, useForkRef, - getCanvasTheme, + getTheme, } from '@workday/canvas-kit-react/common'; import {usePopupModel, usePopupStack} from '@workday/canvas-kit-react/popup'; import {keyframes} from '@emotion/react'; @@ -46,7 +46,7 @@ const Container = styled(Box)({ // the Modal. The centering container forces a "center" pixel calculation by making sure the width // is always an even number const ResponsiveContainer = styled('div')(({theme}) => { - const canvasTheme = getCanvasTheme(theme); + const {canvas: canvasTheme} = getTheme(theme); return { maxHeight: '100vh', display: 'flex',