From f39a65ebbdf49f78760d2e163f0ea555d50ee7b4 Mon Sep 17 00:00:00 2001 From: Trevor <7311041+tjuanitas@users.noreply.github.com> Date: Thu, 16 Jan 2025 13:49:50 -0800 Subject: [PATCH] feat: add theming feature to elements (#3821) * feat: add theming to feature to elements * fix: undefined property * fix: ignore flow check * fix: simplify import path * feat: remove theme generator docs --------- Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com> --- src/elements/common/theming/ThemingStyles.tsx | 10 + .../common/theming/__tests__/utils.test.ts | 48 +++ src/elements/common/theming/index.ts | 2 + src/elements/common/theming/types.ts | 8 + .../common/theming/useCustomTheming.ts | 25 ++ src/elements/common/theming/utils.ts | 23 ++ .../content-explorer/ContentExplorer.js | 7 + src/elements/content-picker/ContentPicker.js | 7 + .../content-preview/ContentPreview.js | 7 + .../content-sidebar/ContentSidebar.js | 5 + src/elements/content-sidebar/Sidebar.js | 7 + .../content-uploader/ContentUploader.tsx | 5 + src/utils/createTheme.stories.js | 278 ------------------ src/utils/createTheme.stories.md | 11 - 14 files changed, 154 insertions(+), 289 deletions(-) create mode 100644 src/elements/common/theming/ThemingStyles.tsx create mode 100644 src/elements/common/theming/__tests__/utils.test.ts create mode 100644 src/elements/common/theming/index.ts create mode 100644 src/elements/common/theming/types.ts create mode 100644 src/elements/common/theming/useCustomTheming.ts create mode 100644 src/elements/common/theming/utils.ts delete mode 100644 src/utils/createTheme.stories.js delete mode 100644 src/utils/createTheme.stories.md diff --git a/src/elements/common/theming/ThemingStyles.tsx b/src/elements/common/theming/ThemingStyles.tsx new file mode 100644 index 0000000000..dac7a4af4e --- /dev/null +++ b/src/elements/common/theming/ThemingStyles.tsx @@ -0,0 +1,10 @@ +import useCustomTheming from './useCustomTheming'; +import { ThemingProps } from './types'; + +const ThemingStyles = ({ selector, theme }: ThemingProps) => { + useCustomTheming({ selector, theme }); + + return null; +}; + +export default ThemingStyles; diff --git a/src/elements/common/theming/__tests__/utils.test.ts b/src/elements/common/theming/__tests__/utils.test.ts new file mode 100644 index 0000000000..b8a41bf42a --- /dev/null +++ b/src/elements/common/theming/__tests__/utils.test.ts @@ -0,0 +1,48 @@ +import { convertTokensToCustomProperties } from '../utils'; + +describe('elements/common/theming/utils', () => { + describe('convertTokensToCustomProperties()', () => { + test('returns correct mapping of tokens to custom properties', () => { + const tokens = { + Label: { + Bold: { + fontSize: '0.625rem', + fontWeight: '700', + letterSpacing: '0.0375rem', + lineHeight: '1rem', + paragraphSpacing: '0', + textCase: 'none', + textDecoration: 'none', + }, + Default: { + fontSize: '0.625rem', + fontWeight: '400', + letterSpacing: '0.0375rem', + lineHeight: '1rem', + paragraphSpacing: '0', + textCase: 'none', + textDecoration: 'none', + }, + }, + }; + const output = { + '--label-bold-font-size': '0.625rem', + '--label-bold-font-weight': '700', + '--label-bold-letter-spacing': '0.0375rem', + '--label-bold-line-height': '1rem', + '--label-bold-paragraph-spacing': '0', + '--label-bold-text-case': 'none', + '--label-bold-text-decoration': 'none', + '--label-default-font-size': '0.625rem', + '--label-default-font-weight': '400', + '--label-default-letter-spacing': '0.0375rem', + '--label-default-line-height': '1rem', + '--label-default-paragraph-spacing': '0', + '--label-default-text-case': 'none', + '--label-default-text-decoration': 'none', + }; + const result = convertTokensToCustomProperties(tokens); + expect(result).toEqual(output); + }); + }); +}); diff --git a/src/elements/common/theming/index.ts b/src/elements/common/theming/index.ts new file mode 100644 index 0000000000..940ff53c95 --- /dev/null +++ b/src/elements/common/theming/index.ts @@ -0,0 +1,2 @@ +export { default } from './ThemingStyles'; +export * from './types'; diff --git a/src/elements/common/theming/types.ts b/src/elements/common/theming/types.ts new file mode 100644 index 0000000000..dd2e78eb50 --- /dev/null +++ b/src/elements/common/theming/types.ts @@ -0,0 +1,8 @@ +export interface Theme { + tokens?: Record; +} + +export interface ThemingProps { + selector?: string; + theme?: Theme; +} diff --git a/src/elements/common/theming/useCustomTheming.ts b/src/elements/common/theming/useCustomTheming.ts new file mode 100644 index 0000000000..c1c6ebfb36 --- /dev/null +++ b/src/elements/common/theming/useCustomTheming.ts @@ -0,0 +1,25 @@ +import { useEffect } from 'react'; +import { convertTokensToCustomProperties } from './utils'; +import { ThemingProps } from './types'; + +const useCustomTheming = ({ selector, theme = {} }: ThemingProps) => { + const { tokens } = theme; + + const customProperties = convertTokensToCustomProperties(tokens); + const styles = Object.entries(customProperties) + .map(([token, value]) => `${token}: ${value}`) + .join(';'); + + useEffect(() => { + const styleEl = document.createElement('style'); + document.head.appendChild(styleEl); + + styleEl.sheet.insertRule(`${selector ?? ':root'} { ${styles} }`); + + return () => { + document.head.removeChild(styleEl); + }; + }, [selector, styles]); +}; + +export default useCustomTheming; diff --git a/src/elements/common/theming/utils.ts b/src/elements/common/theming/utils.ts new file mode 100644 index 0000000000..d93baa5327 --- /dev/null +++ b/src/elements/common/theming/utils.ts @@ -0,0 +1,23 @@ +import isObject from 'lodash/isObject'; +import kebabCase from 'lodash/kebabCase'; + +const convertTokensToCustomProperties = (tokens = {}, prefix = '') => { + const customProperties = {}; + + Object.entries(tokens).forEach(([level, value]) => { + const levelName = `${prefix}${kebabCase(level)}`; + + if (isObject(value)) { + const properties = convertTokensToCustomProperties(value, `${levelName}-`); + Object.entries(properties).forEach(([tokenName, tokenValue]) => { + customProperties[tokenName] = tokenValue; + }); + } else { + customProperties[`--${levelName}`] = value; + } + }); + + return customProperties; +}; + +export { convertTokensToCustomProperties }; diff --git a/src/elements/content-explorer/ContentExplorer.js b/src/elements/content-explorer/ContentExplorer.js index 7983e76e47..645eabbf5d 100644 --- a/src/elements/content-explorer/ContentExplorer.js +++ b/src/elements/content-explorer/ContentExplorer.js @@ -21,6 +21,8 @@ import SubHeader from '../common/sub-header/SubHeader'; import makeResponsive from '../common/makeResponsive'; import openUrlInsideIframe from '../../utils/iframe'; import Internationalize from '../common/Internationalize'; +// $FlowFixMe TypeScript file +import ThemingStyles from '../common/theming'; import API from '../../api'; import MetadataQueryAPIHelper from '../../features/metadata-based-view/MetadataQueryAPIHelper'; import Footer from './Footer'; @@ -67,6 +69,8 @@ import { VIEW_MODE_GRID, } from '../../constants'; import type { ViewMode } from '../common/flowTypes'; +// $FlowFixMe TypeScript file +import type { Theme } from '../common/theming'; import type { MetadataQuery, FieldsToShow } from '../../common/types/metadataQueries'; import type { MetadataFieldValue } from '../../common/types/metadata'; import type { @@ -139,6 +143,7 @@ type Props = { sortDirection: SortDirection, staticHost: string, staticPath: string, + theme?: Theme, token: Token, uploadHost: string, }; @@ -1615,6 +1620,7 @@ class ContentExplorer extends Component { staticHost, staticPath, previewLibraryVersion, + theme, token, uploadHost, }: Props = this.props; @@ -1659,6 +1665,7 @@ class ContentExplorer extends Component { return (
+
{!isDefaultViewMetadata && ( <> diff --git a/src/elements/content-picker/ContentPicker.js b/src/elements/content-picker/ContentPicker.js index 51c90ad6c9..732a3b9125 100644 --- a/src/elements/content-picker/ContentPicker.js +++ b/src/elements/content-picker/ContentPicker.js @@ -18,6 +18,8 @@ import UploadDialog from '../common/upload-dialog'; import CreateFolderDialog from '../common/create-folder-dialog'; import Internationalize from '../common/Internationalize'; import makeResponsive from '../common/makeResponsive'; +// $FlowFixMe TypeScript file +import ThemingStyles from '../common/theming'; import Pagination from '../../features/pagination'; import { isFocusableElement, isInputElement, focus } from '../../utils/dom'; import API from '../../api'; @@ -51,6 +53,8 @@ import { VIEW_SELECTED, } from '../../constants'; import { FILE_SHARED_LINK_FIELDS_TO_FETCH } from '../../utils/fields'; +// $FlowFixMe TypeScript file +import type { Theme } from '../common/theming'; import type { ElementsXhrError } from '../../common/types/api'; import type { View, @@ -114,6 +118,7 @@ type Props = { showSelectedButton: boolean, sortBy: SortBy, sortDirection: SortDirection, + theme?: Theme, token: Token, type: string, uploadHost: string, @@ -1206,6 +1211,7 @@ class ContentPicker extends Component { responseInterceptor, renderCustomActionButtons, showSelectedButton, + theme, }: Props = this.props; const { view, @@ -1234,6 +1240,7 @@ class ContentPicker extends Component { return (
+
{ sharedLinkPassword, requestInterceptor, responseInterceptor, + theme, }: Props = this.props; const { @@ -1326,6 +1332,7 @@ class ContentPreview extends React.PureComponent { onKeyDown={this.onKeyDown} tabIndex={0} > + {hasHeader && ( { onPanelChange, onVersionChange, onVersionHistoryClick, + theme, versionsSidebarProps, }: Props = this.props; const { file, isLoading, metadataEditors }: State = this.state; @@ -404,6 +408,7 @@ class ContentSidebar extends React.Component { onPanelChange={onPanelChange} onVersionChange={onVersionChange} onVersionHistoryClick={onVersionHistoryClick} + theme={theme} versionsSidebarProps={versionsSidebarProps} wrappedComponentRef={ref => { this.sidebarRef = ref; diff --git a/src/elements/content-sidebar/Sidebar.js b/src/elements/content-sidebar/Sidebar.js index 1c3249205b..1855db5777 100644 --- a/src/elements/content-sidebar/Sidebar.js +++ b/src/elements/content-sidebar/Sidebar.js @@ -17,6 +17,8 @@ import LocalStore from '../../utils/LocalStore'; import SidebarNav from './SidebarNav'; import SidebarPanels from './SidebarPanels'; import SidebarUtils from './SidebarUtils'; +// $FlowFixMe TypeScript file +import ThemingStyles from '../common/theming'; import { withCurrentUser } from '../common/current-user'; import { isFeatureEnabled, withFeatureConsumer } from '../common/feature-checking'; import type { FeatureConfig } from '../common/feature-checking'; @@ -30,6 +32,8 @@ import type { AdditionalSidebarTab } from './flowTypes'; import type { MetadataEditor } from '../../common/types/metadata'; import type { BoxItem, User } from '../../common/types/core'; import type { Errors } from '../common/flowTypes'; +// $FlowFixMe TypeScript file +import type { Theme } from '../common/theming'; import { SIDEBAR_VIEW_DOCGEN } from '../../constants'; import API from '../../api'; @@ -65,6 +69,7 @@ type Props = { onPanelChange?: (name: string, isInitialState: boolean) => void, onVersionChange?: Function, onVersionHistoryClick?: Function, + theme?: Theme, versionsSidebarProps: VersionsSidebarProps, }; @@ -304,6 +309,7 @@ class Sidebar extends React.Component { metadataSidebarProps, onAnnotationSelect, onVersionChange, + theme, versionsSidebarProps, }: Props = this.props; const isOpen = this.isOpen(); @@ -321,6 +327,7 @@ class Sidebar extends React.Component { return (