diff --git a/.babelrc.js b/.babelrc.js index bce31e695c2..09f3ccbce9b 100644 --- a/.babelrc.js +++ b/.babelrc.js @@ -13,6 +13,12 @@ module.exports = { }], ["@babel/typescript", { isTSX: true, allExtensions: true }], "@babel/react", + [ + "@emotion/babel-preset-css-prop", + { + "labelFormat": "[filename]-[local]" + }, + ], ], "plugins": [ "@babel/plugin-syntax-dynamic-import", diff --git a/CHANGELOG.md b/CHANGELOG.md index 7e9fc2202ae..1bfe686a273 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,16 @@ ## [`main`](https://github.com/elastic/eui/tree/main) -No public interface changes since `41.4.0`. +### Feature: CSS-in-JS ([#5121](https://github.com/elastic/eui/pull/5121)) + +- Added reset and global styles via CSS-in-JS with `@emotion/react/Global` +- Added `EuiProvider`, a React context provider for theming and global styles +- Added `isDefaultTheme` and `isLegacyTheme` utilities + +**Breaking changes** + +- Added `@emotion/react` to `peerDependencies` +- Amsterdam is now the default theme, deprecated and renamed old theme as "legacy" +- Re-organized Sass files including where the `globals` are imported from ## [`41.4.0`](https://github.com/elastic/eui/tree/v41.4.0) @@ -278,7 +288,7 @@ No public interface changes since `41.4.0`. ## [`37.5.0`](https://github.com/elastic/eui/tree/v37.5.0) -### Feature: Emotion ([#4511](https://github.com/elastic/eui/pull/4511)) +### Feature: CSS-in-JS ([#4511](https://github.com/elastic/eui/pull/4511)) - Added `EuiThemeProvider`, a React context provider for theme values and color mode selection - Added `useEuiTheme` React hook, and `withEuiTheme` React HOC for consuming the EuiTheme diff --git a/README.md b/README.md index 9d4d689a66a..cb6e31ed25d 100644 --- a/README.md +++ b/README.md @@ -6,20 +6,7 @@ Check out our [full documentation site][docs] which contains many examples of components in the EUI framework aesthetic, and how to use them in your products. We also have a [FAQ][faq] that covers common usage questions. For other general questions regarding EUI, check out the [Discussions tab](https://github.com/elastic/eui/discussions). -## Installation - -To install the Elastic UI Framework into an existing project, use the `yarn` CLI (`npm` is not supported). - -``` -yarn add @elastic/eui -``` - -Note that EUI has [several `peerDependencies` requirements](package.json) that will also need to be installed if starting with a blank project. You can read more about other ways to [consume EUI][consuming]. - -``` -yarn add @elastic/eui @elastic/datemath moment prop-types -``` - +The rest of this doc will detail how to run and contribute to the **EUI documentation** site locally. ## Running Locally @@ -31,7 +18,7 @@ You will probably want to install a node version manager. [nvm](https://github.c To install and use the correct node version with `nvm`: -``` +```bash nvm install ``` @@ -39,14 +26,14 @@ nvm install You can run the documentation locally at [http://localhost:8030/](http://localhost:8030/) by running the following. -``` +```bash yarn yarn start ``` If another process is already listening on port 8030, the next free port will be used. Alternatively, you can specify a port: -``` +```bash yarn start --port 9000 ``` @@ -75,7 +62,6 @@ directly in the code. And unit test coverage for the UI components allows us to * [Creating components manually](wiki/creating-components-manually.md) * [Creating components with Yeoman](wiki/creating-components-yeoman.md) * [Creating icons](wiki/creating-icons.md) -* [Theming](wiki/theming.md) * [Testing](wiki/testing.md) * [Accessibility Testing](wiki/automated-accessibility-testing.md) * [Documentation](wiki/documentation-guidelines.md) diff --git a/cypress/plugins/webpack.config.js b/cypress/plugins/webpack.config.js index 9f4eeef1ab8..32ef33e03ed 100644 --- a/cypress/plugins/webpack.config.js +++ b/cypress/plugins/webpack.config.js @@ -11,7 +11,7 @@ const path = require('path'); const webpack = require('webpack'); -const THEME_IMPORT = `'../../dist/eui_theme_amsterdam_${process.env.THEME}.css'`; +const THEME_IMPORT = `'../../dist/eui_theme_${process.env.THEME}.css'`; module.exports = { mode: 'development', diff --git a/package.json b/package.json index 4d028017a19..b215eec2196 100644 --- a/package.json +++ b/package.json @@ -113,7 +113,9 @@ "@elastic/datemath": "^5.0.3", "@elastic/eslint-config-kibana": "^0.15.0", "@emotion/babel-preset-css-prop": "^11.0.0", + "@emotion/cache": "^11.4.0", "@emotion/eslint-plugin": "^11.0.0", + "@emotion/jest": "^11.1.0", "@emotion/react": "^11.1.1", "@svgr/core": "5.5.0", "@svgr/plugin-svgo": "^4.0.3", @@ -234,6 +236,7 @@ }, "peerDependencies": { "@elastic/datemath": "^5.0.2", + "@emotion/react": "11.x", "@types/react": "^16.9.34", "@types/react-dom": "^16.9.6", "moment": "^2.13.0", diff --git a/scripts/compile-scss.js b/scripts/compile-scss.js index 95d256cb7d9..4f6d13faca3 100755 --- a/scripts/compile-scss.js +++ b/scripts/compile-scss.js @@ -41,7 +41,7 @@ async function compileScssFiles({ const inputFilenames = (await glob(sourcePattern, undefined)).filter(filename => { if (targetTheme == null) return true; - return filename === `src/theme_${targetTheme}.scss`; + return filename === `src/themes/amsterdam/theme_${targetTheme}.scss`; }); await Promise.all( @@ -147,7 +147,14 @@ if (require.main === module) { } compileScssFiles({ - sourcePattern: path.join('src', 'theme_*.scss'), + sourcePattern: path.join('src/themes/legacy', 'legacy_*.scss'), + destinationDirectory: 'dist', + docsVariablesDirectory: 'src-docs/src/views/theme/_json', + packageName: euiPackageName + }); + + compileScssFiles({ + sourcePattern: path.join('src/themes/amsterdam', 'theme_*.scss'), destinationDirectory: 'dist', docsVariablesDirectory: 'src-docs/src/views/theme/_json', packageName: euiPackageName diff --git a/scripts/cypress.js b/scripts/cypress.js index 1bf778720d3..b4bb5f5e08c 100644 --- a/scripts/cypress.js +++ b/scripts/cypress.js @@ -1,19 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + const { execSync } = require('child_process'); const chalk = require('chalk'); -const yargs = require('yargs/yargs') -const { hideBin } = require('yargs/helpers') +const yargs = require('yargs/yargs'); +const { hideBin } = require('yargs/helpers'); const argv = yargs(hideBin(process.argv)) .parserConfiguration({ - "camel-case-expansion": false, // don't convert dash-separated options into camel case (e.g. dashSeparated) - "unknown-options-as-args": true, // collect any extra options to pass on to cypress + 'camel-case-expansion': false, // don't convert dash-separated options into camel case (e.g. dashSeparated) + 'unknown-options-as-args': true, // collect any extra options to pass on to cypress }) .options({ 'skip-css': { type: 'boolean' }, - 'dev': { type: 'boolean' }, - 'theme': { type: 'string', default: 'light', choices: ['light', 'dark'] }, - }) - .argv + dev: { type: 'boolean' }, + theme: { type: 'string', default: 'light', choices: ['light', 'dark'] }, + }).argv; const isDev = argv.hasOwnProperty('dev'); const skipScss = argv.hasOwnProperty('skip-css'); @@ -25,12 +32,9 @@ const log = chalk.grey; // compile scss -> css so tests can render correctly if (!skipScss) { console.log(info('Compiling SCSS')); - execSync( - `TARGET_THEME=amsterdam_${theme} yarn compile-scss`, - { - stdio: 'inherit' - } - ); + execSync(`TARGET_THEME=${theme} yarn compile-scss`, { + stdio: 'inherit', + }); } else { console.log(info('Not compiling SCSS, disabled by --skip-css')); } @@ -41,15 +45,12 @@ const cypressCommandParts = [ 'BABEL_MODULES=false', // let webpack receive ES Module code 'NODE_ENV=cypress_test', // enable code coverage checks `cypress ${isDev ? 'open-ct' : 'run-ct'}`, - ...argv._ // pass any extra options given to this script + ...argv._, // pass any extra options given to this script ]; const cypressCommand = cypressCommandParts.join(' '); console.log(info(`${isDev ? 'Opening' : 'Running'} cypress`)); -console.log(log(cypressCommand)) -execSync( - cypressCommand, - { - stdio: 'inherit' - } -); +console.log(log(cypressCommand)); +execSync(cypressCommand, { + stdio: 'inherit', +}); diff --git a/scripts/jest/config.json b/scripts/jest/config.json index 0e65d60e4ce..eb18fffaa1a 100644 --- a/scripts/jest/config.json +++ b/scripts/jest/config.json @@ -46,6 +46,7 @@ "^.+\\.(js|tsx?)$": "babel-jest" }, "snapshotSerializers": [ - "/node_modules/enzyme-to-json/serializer" + "/node_modules/enzyme-to-json/serializer", + "@emotion/jest/enzyme-serializer" ] } diff --git a/scripts/release.js b/scripts/release.js index 15050070ebb..51f74144313 100644 --- a/scripts/release.js +++ b/scripts/release.js @@ -155,13 +155,13 @@ async function getVersionTypeFromChangelog() { // // "##.+?[\r\n]+" consume the first heading & linebreak(s), which describes the main branch // "(.+?)" capture (non-greedy) all changes until the rest of the regex matches - // "[\r\n]+##" any linebreak(s) leading up to the next ## heading + // "[\r\n]+##(?= \[`\d)" any linebreak(s) leading up to the next ## heading with a numbered release, e.g. [`1.0.0`] // // regex flags "su" enable dotAll (s) and unicode-character matching (u) // // effectively capturing pending changes in the capture group // which is stored as the second item in the returned array from `changelog.match()` - const [, unreleasedchanges] = changelog.match(/##.+?[\r\n]+(.+?)[\r\n]+##/su); + const [, unreleasedchanges] = changelog.match(/##.+?[\r\n]+(.+?)[\r\n]+##(?= \[`\d)/su); // these changes contain bug fixes if the string "**bug fixes**" exists const hasBugFixes = unreleasedchanges.toLowerCase().indexOf('**bug fixes**') !== -1; diff --git a/src-docs/.babelrc.js b/src-docs/.babelrc.js index ab09003d064..a80333ae140 100644 --- a/src-docs/.babelrc.js +++ b/src-docs/.babelrc.js @@ -2,12 +2,6 @@ const baseConfig = require('../.babelrc.js'); const index = baseConfig.plugins.indexOf( './scripts/babel/proptypes-from-ts-props' ); -baseConfig.presets.push([ - '@emotion/babel-preset-css-prop', - { - labelFormat: '[local]', - }, -]); baseConfig.plugins.splice( index + 1, 0, diff --git a/src-docs/src/components/codesandbox/link.js b/src-docs/src/components/codesandbox/link.js index 68bc61e8f0c..f62f3bb0753 100644 --- a/src-docs/src/components/codesandbox/link.js +++ b/src-docs/src/components/codesandbox/link.js @@ -5,6 +5,9 @@ import { hasDisplayToggles, listExtraDeps, } from '../../services'; +import { LEGACY_NAME_KEY } from '../../../../src/themes'; + +import { ThemeContext } from '../with_theme'; const pkg = require('../../../../package.json'); @@ -26,7 +29,6 @@ const getVersion = (packageName) => { * 5. Through regex we read the dependencies of both `content` and `display_toggles` and pass that to CS. * 6. We pass the files and dependencies as params to CS through a POST call. * */ -import { ThemeContext } from '../with_theme'; const displayTogglesRawCode = require('!!raw-loader!../../views/form_controls/display_toggles') .default; @@ -49,11 +51,11 @@ export const CodeSandboxLinkComponent = ({ }) => { let cssFile; switch (context.theme) { - case 'amsterdam-light': - cssFile = '@elastic/eui/dist/eui_theme_amsterdam_light.css'; + case `${LEGACY_NAME_KEY}_light`: + cssFile = '@elastic/eui/dist/eui_legacy_light.css'; break; - case 'amsterdam-dark': - cssFile = '@elastic/eui/dist/eui_theme_amsterdam_dark.css'; + case `${LEGACY_NAME_KEY}_dark`: + cssFile = '@elastic/eui/dist/eui_legacy_dark.css'; break; case 'dark': cssFile = '@elastic/eui/dist/eui_theme_dark.css'; @@ -63,6 +65,33 @@ export const CodeSandboxLinkComponent = ({ break; } + const isLegacyTheme = context.theme.includes(LEGACY_NAME_KEY); + + const providerPropsObject = {}; + // Only add configuration if it isn't the default + if (context.theme.includes('dark')) { + providerPropsObject.colorMode = 'dark'; + } + // Can't spread an object inside of a string literal + const providerProps = Object.keys(providerPropsObject) + .map((prop) => { + const value = providerPropsObject[prop]; + return value !== null ? `${prop}="${value}"` : `${prop}={${value}}`; + }) + .join(' '); + + // Renders the new Demo component generically into the code sandbox page + const exampleClose = `ReactDOM.render( + ${ + isLegacyTheme + ? '' + : ` + + ` + }, + document.getElementById('root') +);`; + let indexContent; if (!content) { @@ -72,28 +101,48 @@ import '${cssFile}'; import React from 'react'; import { - EuiButton, + ${ + isLegacyTheme + ? 'EuiButton,' + : `EuiButton, + EuiProvider,` + } } from '@elastic/eui'; const Demo = () => (Hello world!); -ReactDOM.render( - , - document.getElementById('root') -); +${exampleClose} `; } else { /** This cleans the Demo JS example for Code Sanbox. - Replaces relative imports with pure @elastic/eui ones + - Adds provider import, if necessary - Changes the JS example from a default export to a component const named Demo **/ - const exampleCleaned = cleanEuiImports(content) + let exampleCleaned = cleanEuiImports(content) .replace('export default', 'const Demo =') .replace( /(from )'(..\/)+display_toggles(\/?';)/, "from './display_toggles';" ); + if (!isLegacyTheme && !exampleCleaned.includes('EuiProvider')) { + if (exampleCleaned.includes(" } from '@elastic/eui';")) { + // Single line import statement + exampleCleaned = exampleCleaned.replace( + " } from '@elastic/eui';", + ", EuiProvider } from '@elastic/eui';" + ); + } else { + // Multi line import statement + exampleCleaned = exampleCleaned.replace( + "} from '@elastic/eui';", + ` EuiProvider, +} from '@elastic/eui';` + ); + } + } + // If the code example still has local doc imports after the above cleaning it's // too complicated for code sandbox so we don't provide a link const hasLocalImports = /(from )'((.|..)\/).*?';/.test(exampleCleaned); @@ -102,11 +151,6 @@ ReactDOM.render( return null; } - // Renders the new Demo component generically into the code sandbox page - const exampleClose = `ReactDOM.render( - , - document.getElementById('root') - );`; // The Code Sanbbox demo needs to import CSS at the top of the document. CS has trouble // with our dynamic imports so we need to warn the user for now const exampleStart = `import ReactDOM from 'react-dom'; @@ -116,7 +160,7 @@ import '${cssFile}';`; const cleanedContent = `${exampleStart} ${exampleCleaned} ${exampleClose} - `; +`; indexContent = cleanedContent.replace( /(from )'.+display_toggles';/, "from './display_toggles';" diff --git a/src-docs/src/components/guide_page/index.js b/src-docs/src/components/guide_page/index.js index b401f0fe0e1..9d0d6aed863 100644 --- a/src-docs/src/components/guide_page/index.js +++ b/src-docs/src/components/guide_page/index.js @@ -1,3 +1,5 @@ export { GuidePage } from './guide_page'; export { GuidePageChrome } from './guide_page_chrome'; + +export { GuidePageHeader } from './guide_page_header'; diff --git a/src-docs/src/components/guide_section/guide_section_parts/guide_section_props_table.tsx b/src-docs/src/components/guide_section/guide_section_parts/guide_section_props_table.tsx index bccd36828bf..76f3bdb9fee 100644 --- a/src-docs/src/components/guide_section/guide_section_parts/guide_section_props_table.tsx +++ b/src-docs/src/components/guide_section/guide_section_parts/guide_section_props_table.tsx @@ -9,7 +9,7 @@ import Knobs from '../../../services/playground/knobs'; import { propUtilityForPlayground } from '../../../services/playground'; export type GuideSectionPropsTable = { - componentName: any; + componentName?: any; component: any; }; @@ -25,10 +25,12 @@ export const GuideSectionPropsTable: FunctionComponent = return (
- + {componentName && ( + + )} = ({ }) => { const isMobileSize = useIsWithinBreakpoints(['xs', 's']); - const isAmsterdam = context.theme.includes('amsterdam'); + const isLegacy = context.theme.includes('legacy'); - let href = 'https://www.figma.com/community/file/809845546262698150'; + let href = 'https://www.figma.com/community/file/964536385682658129'; const label = 'EUI Figma Design Library'; - if (isAmsterdam) { - href = 'https://www.figma.com/community/file/964536385682658129'; + if (isLegacy) { + href = 'https://www.figma.com/community/file/809845546262698150'; } return isMobileSize ? ( diff --git a/src-docs/src/components/guide_theme_selector/guide_sketch_link.tsx b/src-docs/src/components/guide_theme_selector/guide_sketch_link.tsx index e6e0b2a8d2a..73dced9c5bf 100644 --- a/src-docs/src/components/guide_theme_selector/guide_sketch_link.tsx +++ b/src-docs/src/components/guide_theme_selector/guide_sketch_link.tsx @@ -31,9 +31,9 @@ const GuideSketchLinkComponent: React.FunctionComponent = 'https://github.com/elastic/eui/releases/download/v8.0.0/eui_sketch_8.0.0.zip'; const label = 'EUI Sketch Library (download)'; - const isAmsterdam = context.theme.includes('amsterdam'); + const isLegacy = context.theme.includes('legacy'); - if (isAmsterdam) return <>; + if (!isLegacy) return <>; return isMobileSize ? ( diff --git a/src-docs/src/components/guide_theme_selector/guide_theme_selector.tsx b/src-docs/src/components/guide_theme_selector/guide_theme_selector.tsx index 6b28312b90a..0d474e8bfbb 100644 --- a/src-docs/src/components/guide_theme_selector/guide_theme_selector.tsx +++ b/src-docs/src/components/guide_theme_selector/guide_theme_selector.tsx @@ -1,19 +1,23 @@ /* eslint-disable no-restricted-globals */ import React, { useState } from 'react'; -import { EuiButton } from '../../../../src/components/button'; -import { - EuiContextMenuPanel, - EuiContextMenuItem, -} from '../../../../src/components/context_menu'; -import { EuiPopover } from '../../../../src/components/popover'; -import { EuiHorizontalRule } from '../../../../src/components/horizontal_rule'; import { useIsWithinBreakpoints } from '../../../../src/services/hooks/useIsWithinBreakpoints'; import { EUI_THEME, EUI_THEMES } from '../../../../src/themes'; import { ThemeContext } from '../with_theme'; // @ts-ignore Not TS import { GuideLocaleSelector } from '../guide_locale_selector'; +import { + EuiText, + EuiTourStep, + EuiPopover, + EuiHorizontalRule, + EuiButton, + EuiContextMenuPanel, + EuiContextMenuItem, + EuiLink, + EuiIcon, +} from '../../../../src/components'; type GuideThemeSelectorProps = { onToggleLocale: () => {}; @@ -31,6 +35,8 @@ export const GuideThemeSelector: React.FunctionComponent = ({ context, @@ -39,9 +45,19 @@ const GuideThemeSelectorComponent: React.FunctionComponent { const isMobileSize = useIsWithinBreakpoints(['xs', 's']); const [isPopoverOpen, setPopover] = useState(false); + const [isOpen, setIsOpen] = useState( + localStorage.getItem(STORAGE_KEY) !== 'dismissed' + ); + + const onTourDismiss = () => { + setIsOpen(false); + localStorage.setItem(STORAGE_KEY, 'dismissed'); + }; const onButtonClick = () => { setPopover(!isPopoverOpen); + setIsOpen(false); + localStorage.setItem(STORAGE_KEY, 'dismissed'); }; const closePopover = () => { @@ -84,27 +100,53 @@ const GuideThemeSelectorComponent: React.FunctionComponent - - {location.host.includes('803') && ( + +

+ Amsterdam is now the default theme and the old theme is considered + legacy. Our{' '} + + setup instructions + {' '} + will sync with the currently selected theme. +

+ + } + isStepOpen={isOpen} + onFinish={onTourDismiss} + step={1} + stepsTotal={1} + title={ <> - -
- -
+   Theming update - )} - + } + footerAction={Got it!} + repositionOnScroll + > + + + {location.host.includes('803') && ( + <> + +
+ +
+ + )} +
+
); }; diff --git a/src-docs/src/components/index.js b/src-docs/src/components/index.js index 72de5902766..d5c7991c3da 100644 --- a/src-docs/src/components/index.js +++ b/src-docs/src/components/index.js @@ -7,7 +7,7 @@ export { export { GuideMarkdownFormat } from './guide_markdown_format'; -export { GuidePage, GuidePageChrome } from './guide_page'; +export { GuidePage, GuidePageChrome, GuidePageHeader } from './guide_page'; export { GuideSectionContainer as GuideSection } from './guide_section/guide_section_container'; diff --git a/src-docs/src/components/with_theme/language_selector.tsx b/src-docs/src/components/with_theme/language_selector.tsx index 50a5d308337..93e4e75ce62 100644 --- a/src-docs/src/components/with_theme/language_selector.tsx +++ b/src-docs/src/components/with_theme/language_selector.tsx @@ -15,11 +15,14 @@ import { } from './theme_context'; const NOTIF_STORAGE_KEY = 'js_vs_sass_notification'; +const NOTIF_STORAGE_VALUE = 'dismissed'; export const LanguageSelector = ({ onChange, + showTour = true, }: { onChange?: (id: string) => void; + showTour?: boolean; }) => { const themeContext = useContext(ThemeContext); const toggleIdSelected = themeContext.themeLanguage; @@ -27,16 +30,18 @@ export const LanguageSelector = ({ themeContext.changeThemeLanguage(optionId as THEME_LANGUAGES['id']); onChange?.(optionId); setTourIsOpen(false); - localStorage.setItem(NOTIF_STORAGE_KEY, 'dismissed'); + localStorage.setItem(NOTIF_STORAGE_KEY, NOTIF_STORAGE_VALUE); }; const [isTourOpen, setTourIsOpen] = useState( - localStorage.getItem(NOTIF_STORAGE_KEY) !== 'dismissed' + localStorage.getItem(NOTIF_STORAGE_KEY) === NOTIF_STORAGE_VALUE + ? false + : showTour ); const onTourDismiss = () => { setTourIsOpen(false); - localStorage.setItem(NOTIF_STORAGE_KEY, 'dismissed'); + localStorage.setItem(NOTIF_STORAGE_KEY, NOTIF_STORAGE_VALUE); }; return ( diff --git a/src-docs/src/components/with_theme/theme_context.tsx b/src-docs/src/components/with_theme/theme_context.tsx index 92510ef9263..93cd1bbb2f0 100644 --- a/src-docs/src/components/with_theme/theme_context.tsx +++ b/src-docs/src/components/with_theme/theme_context.tsx @@ -2,9 +2,6 @@ import React from 'react'; import { EUI_THEMES, EUI_THEME } from '../../../../src/themes'; // @ts-ignore importing from a JS file import { applyTheme } from '../../services'; -import { EuiThemeProvider } from '../../../../src/services'; -import { EuiThemeAmsterdam } from '../../../../src/themes/eui-amsterdam/theme'; -import { EuiThemeDefault } from '../../../../src/themes/eui/theme'; export const STYLE_STORAGE_KEY = 'js_vs_sass_preference'; @@ -92,14 +89,7 @@ export class ThemeProvider extends React.Component { changeThemeLanguage: this.changeThemeLanguage, }} > - - {children} - + {children} ); } diff --git a/src-docs/src/index.html b/src-docs/src/index.html index 6a8eba081c2..6823472c33e 100644 --- a/src-docs/src/index.html +++ b/src-docs/src/index.html @@ -14,6 +14,7 @@ +
diff --git a/src-docs/src/index.js b/src-docs/src/index.js index 02734e25eea..7ad3536ea95 100644 --- a/src-docs/src/index.js +++ b/src-docs/src/index.js @@ -2,27 +2,29 @@ import React, { createElement } from 'react'; import ReactDOM from 'react-dom'; import { Provider } from 'react-redux'; import { Router, Switch, Route, Redirect } from 'react-router'; +import { Helmet } from 'react-helmet'; import configureStore, { history } from './store/configure_store'; -import { AppContainer } from './views/app_container'; +import { AppContext } from './views/app_context'; +import { AppView } from './views/app_view'; import { HomeView } from './views/home/home_view'; import { NotFoundView } from './views/not_found/not_found_view'; import { registerTheme, ExampleContext } from './services'; import Routes from './routes'; +import legacyThemeLight from './legacy_light.scss'; +import legacyThemeDark from './legacy_dark.scss'; import themeLight from './theme_light.scss'; import themeDark from './theme_dark.scss'; -import themeAmsterdamLight from './theme_amsterdam_light.scss'; -import themeAmsterdamDark from './theme_amsterdam_dark.scss'; import { ThemeProvider } from './components/with_theme/theme_context'; import ScrollToHash from './components/scroll_to_hash'; -import { LinkWrapper } from './views/link_wrapper'; +import { LEGACY_NAME_KEY } from '../../src/themes'; registerTheme('light', [themeLight]); registerTheme('dark', [themeDark]); -registerTheme('amsterdam-light', [themeAmsterdamLight]); -registerTheme('amsterdam-dark', [themeAmsterdamDark]); +registerTheme(`${LEGACY_NAME_KEY}_light`, [legacyThemeLight]); +registerTheme(`${LEGACY_NAME_KEY}_dark`, [legacyThemeDark]); // Set up app @@ -47,72 +49,86 @@ const routes = [ ReactDOM.render( - - - - {routes.map( - ({ name, path, sections, isNew, component, from, to }) => { - const mainComponent = ( - { - const { location } = props; - // prevents encoded urls with a section id to fail - if (location.pathname.includes('%23')) { - const url = decodeURIComponent(location.pathname); - return ; - } else { - return ( - - + + + + {routes.map( + ({ name, path, sections, isNew, component, from, to }) => { + const meta = ( + + {`${name} - Elastic UI Framework`} + + ); + const mainComponent = ( + { + const { location } = props; + // prevents encoded urls with a section id to fail + if (location.pathname.includes('%23')) { + const url = decodeURIComponent(location.pathname); + return ; + } else { + return ( + - {createElement(component, {})} - - - ); - } - }} - /> - ); + {({ theme }) => ( + <> + {meta} + {createElement(component, { + selectedTheme: theme, + title: name, + })} + + )} + + ); + } + }} + /> + ); - const standaloneSections = (sections || []) - .map(({ id, fullScreen }) => { - if (!fullScreen) return undefined; - const { slug, demo } = fullScreen; - return ( - ( - - {demo} - - )} - /> - ); - }) - .filter((x) => !!x); + const standaloneSections = (sections || []) + .map(({ id, fullScreen }) => { + if (!fullScreen) return undefined; + const { slug, demo } = fullScreen; + return ( + ( + + {meta} + {demo} + + )} + /> + ); + }) + .filter((x) => !!x); - // place standaloneSections before mainComponent so their routes take precedent - const routes = [...standaloneSections, mainComponent]; + // place standaloneSections before mainComponent so their routes take precedent + const routes = [...standaloneSections, mainComponent]; - if (from) - return [ - ...routes, - - - , - ]; - else if (component) return routes; - return null; - } - )} - - + if (from) + return [ + ...routes, + + + , + ]; + else if (component) return routes; + return null; + } + )} + + + , document.getElementById('guide') diff --git a/src-docs/src/theme_amsterdam_dark.scss b/src-docs/src/legacy_dark.scss similarity index 54% rename from src-docs/src/theme_amsterdam_dark.scss rename to src-docs/src/legacy_dark.scss index 90a5670c5c1..37c520ddaf8 100644 --- a/src-docs/src/theme_amsterdam_dark.scss +++ b/src-docs/src/legacy_dark.scss @@ -1,7 +1,8 @@ // sass-lint:disable no-url-domains, no-url-protocols -@import url('https://fonts.googleapis.com/css2?family=Inter:slnt,wght@-10,300..700;0,300..700&family=Roboto+Mono:ital,wght@0,400..700;1,400..700&display=swap'); +@import url('https://fonts.googleapis.com/css?family=Roboto+Mono:400,400i,700,700i'); +@import url('https://rsms.me/inter/inter-ui.css'); -@import '../../src/theme_amsterdam_dark'; +@import '../../src/themes/legacy/legacy_dark'; @import './components/index'; @import './services/playground/index'; @import './views/index'; diff --git a/src-docs/src/theme_amsterdam_light.scss b/src-docs/src/legacy_light.scss similarity index 54% rename from src-docs/src/theme_amsterdam_light.scss rename to src-docs/src/legacy_light.scss index 87deafd9666..03057af25c6 100644 --- a/src-docs/src/theme_amsterdam_light.scss +++ b/src-docs/src/legacy_light.scss @@ -1,7 +1,8 @@ // sass-lint:disable no-url-domains, no-url-protocols -@import url('https://fonts.googleapis.com/css2?family=Inter:slnt,wght@-10,300..700;0,300..700&family=Roboto+Mono:ital,wght@0,400..700;1,400..700&display=swap'); +@import url('https://fonts.googleapis.com/css?family=Roboto+Mono:400,400i,700,700i'); +@import url('https://rsms.me/inter/inter-ui.css'); -@import '../../src/theme_amsterdam_light'; +@import '../../src/themes/legacy/legacy_light'; @import './components/index'; @import './services/playground/index'; @import './views/index'; diff --git a/src-docs/src/routes.js b/src-docs/src/routes.js index 70ecdc0dedb..c13fea34741 100644 --- a/src-docs/src/routes.js +++ b/src-docs/src/routes.js @@ -13,7 +13,7 @@ import { EuiErrorBoundary } from '../../src/components'; import { playgroundCreator } from './services/playground'; // Guidelines -const GettingStarted = require('!!raw-loader!./views/guidelines/getting_started.md'); +import { GettingStarted } from './views/guidelines/getting_started/getting_started'; import AccessibilityGuidelines from './views/guidelines/accessibility'; @@ -176,6 +176,8 @@ import { PortalExample } from './views/portal/portal_example'; import { ProgressExample } from './views/progress/progress_example'; +import { ProviderExample } from './views/provider/provider_example'; + import { RangeControlExample } from './views/range/range_example'; import { TreeViewExample } from './views/tree_view/tree_view_example'; @@ -352,8 +354,9 @@ const createTabbedPage = ({ }; }; -const createMarkdownExample = (example, title) => { - const headings = example.default.match(/^(##) (.*)/gm); +// eslint-disable-next-line @typescript-eslint/no-unused-vars +const createMarkdownExample = (file, name, intro) => { + const headings = file.default.match(/^(##) (.*)/gm); const sections = headings.map((heading) => { const title = heading.replace('## ', ''); @@ -362,12 +365,10 @@ const createMarkdownExample = (example, title) => { }); return { - name: title, + name, component: () => ( - - - {example.default} - + + {file.default} ), sections: sections, @@ -378,7 +379,7 @@ const navigation = [ { name: 'Guidelines', items: [ - createMarkdownExample(GettingStarted, 'Getting started'), + createExample(GettingStarted, 'Getting started'), createExample(AccessibilityGuidelines, 'Accessibility'), createTabbedPage({ title: 'Writing', @@ -579,6 +580,7 @@ const navigation = [ OverlayMaskExample, PortalExample, PrettyDurationExample, + ProviderExample, ResizeObserverExample, ResponsiveExample, TextDiffExample, diff --git a/src-docs/src/services/theme/theme.js b/src-docs/src/services/theme/theme.js index 04c44ee2246..a3bbf6030c2 100644 --- a/src-docs/src/services/theme/theme.js +++ b/src-docs/src/services/theme/theme.js @@ -8,5 +8,5 @@ export function applyTheme(newTheme) { Object.keys(themes).forEach((theme) => themes[theme].forEach((cssFile) => cssFile.unuse()) ); - themes[newTheme].forEach((cssFile) => cssFile.use()); + themes[newTheme]?.forEach((cssFile) => cssFile.use()); } diff --git a/src-docs/src/theme_dark.scss b/src-docs/src/theme_dark.scss index 15dd00c3593..44e0626e7ed 100644 --- a/src-docs/src/theme_dark.scss +++ b/src-docs/src/theme_dark.scss @@ -1,8 +1,7 @@ // sass-lint:disable no-url-domains, no-url-protocols -@import url('https://fonts.googleapis.com/css?family=Roboto+Mono:400,400i,700,700i'); -@import url('https://rsms.me/inter/inter-ui.css'); +@import url('https://fonts.googleapis.com/css2?family=Inter:slnt,wght@-10,300..700;0,300..700&family=Roboto+Mono:ital,wght@0,400..700;1,400..700&display=swap'); -@import '../../src/theme_dark'; +@import '../../src/themes/amsterdam/theme_dark'; @import './components/index'; @import './services/playground/index'; @import './views/index'; diff --git a/src-docs/src/theme_light.scss b/src-docs/src/theme_light.scss index b10737d78ae..93c83edf4d5 100644 --- a/src-docs/src/theme_light.scss +++ b/src-docs/src/theme_light.scss @@ -1,8 +1,7 @@ // sass-lint:disable no-url-domains, no-url-protocols -@import url('https://fonts.googleapis.com/css?family=Roboto+Mono:400,400i,700,700i'); -@import url('https://rsms.me/inter/inter-ui.css'); +@import url('https://fonts.googleapis.com/css2?family=Inter:slnt,wght@-10,300..700;0,300..700&family=Roboto+Mono:ital,wght@0,400..700;1,400..700&display=swap'); -@import '../../src/theme_light'; +@import '../../src/themes/amsterdam/theme_light'; @import './components/index'; @import './services/playground/index'; @import './views/index'; diff --git a/src-docs/src/views/app_container.js b/src-docs/src/views/app_container.js deleted file mode 100644 index 891532f1359..00000000000 --- a/src-docs/src/views/app_container.js +++ /dev/null @@ -1,22 +0,0 @@ -import { connect } from 'react-redux'; -import { withRouter } from 'react-router-dom'; - -import { AppView } from './app_view'; -import { getRoutes, getLocale } from '../store'; - -import { toggleLocale } from '../actions'; - -function mapStateToProps(state, ownProps) { - return { - currentRoute: ownProps.currentRoute, - locale: getLocale(state), - routes: getRoutes(state), - ...ownProps, - }; -} - -export const AppContainer = withRouter( - connect(mapStateToProps, { - toggleLocale, - })(AppView) -); diff --git a/src-docs/src/views/app_context.js b/src-docs/src/views/app_context.js new file mode 100644 index 00000000000..ee46d60ffa3 --- /dev/null +++ b/src-docs/src/views/app_context.js @@ -0,0 +1,79 @@ +import PropTypes from 'prop-types'; +import React, { useContext } from 'react'; +import { Helmet } from 'react-helmet'; +import { useSelector } from 'react-redux'; +import createCache from '@emotion/cache'; +import { ThemeContext } from '../components'; +import { translateUsingPseudoLocale } from '../services'; +import { getLocale } from '../store'; + +import { EuiContext, EuiProvider } from '../../../src/components'; +import { EUI_THEMES } from '../../../src/themes'; + +import favicon16Prod from '../images/favicon/prod/favicon-16x16.png'; +import favicon32Prod from '../images/favicon/prod/favicon-32x32.png'; +import favicon96Prod from '../images/favicon/prod/favicon-96x96.png'; +import favicon16Dev from '../images/favicon/dev/favicon-16x16.png'; +import favicon32Dev from '../images/favicon/dev/favicon-32x32.png'; +import favicon96Dev from '../images/favicon/dev/favicon-96x96.png'; + +const emotionCache = createCache({ + key: 'eui-docs', + container: document.querySelector('#emotion-global-insert'), +}); + +export const AppContext = ({ children }) => { + const { theme } = useContext(ThemeContext); + const locale = useSelector((state) => getLocale(state)); + + const mappingFuncs = { + 'en-xa': translateUsingPseudoLocale, + }; + + const i18n = { + mappingFunc: mappingFuncs[locale], + formatNumber: (value) => new Intl.NumberFormat(locale).format(value), + locale, + }; + + const isLocalDev = window.location.host.includes('803'); + + return ( + t.value === theme)?.provider} + colorMode={theme.includes('light') ? 'light' : 'dark'} + > + + + + + + {children} + + ); +}; + +AppContext.propTypes = { + children: PropTypes.any, + currentRoute: PropTypes.object.isRequired, +}; + +AppContext.defaultProps = { + currentRoute: {}, +}; diff --git a/src-docs/src/views/app_view.js b/src-docs/src/views/app_view.js index cf33804176c..07c08091325 100644 --- a/src-docs/src/views/app_view.js +++ b/src-docs/src/views/app_view.js @@ -1,151 +1,93 @@ import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import { Helmet } from 'react-helmet'; -import { GuidePageChrome, ThemeContext } from '../components'; -import { translateUsingPseudoLocale } from '../services'; +import React, { useContext, useEffect, useRef } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { toggleLocale as _toggleLocale } from '../actions'; +import { GuidePageChrome, ThemeContext, GuidePageHeader } from '../components'; +import { getLocale, getRoutes } from '../store'; import { EuiErrorBoundary, EuiPage, - EuiContext, EuiPageBody, } from '../../../src/components'; import { keys } from '../../../src/services'; -import { GuidePageHeader } from '../components/guide_page/guide_page_header'; - -import favicon16Prod from '../images/favicon/prod/favicon-16x16.png'; -import favicon32Prod from '../images/favicon/prod/favicon-32x32.png'; -import favicon96Prod from '../images/favicon/prod/favicon-96x96.png'; -import favicon16Dev from '../images/favicon/dev/favicon-16x16.png'; -import favicon32Dev from '../images/favicon/dev/favicon-32x32.png'; -import favicon96Dev from '../images/favicon/dev/favicon-96x96.png'; - -export class AppView extends Component { - componentDidUpdate(prevProps) { - if (prevProps.currentRoute.path !== this.props.currentRoute.path) { - window.scrollTo(0, 0); - } - } - componentDidMount() { - document.addEventListener('keydown', this.onKeydown); - } +import { LinkWrapper } from './link_wrapper'; - componentWillUnmount() { - document.removeEventListener('keydown', this.onKeydown); - } +export const AppView = ({ children, currentRoute }) => { + const dispatch = useDispatch(); + const toggleLocale = (locale) => dispatch(_toggleLocale(locale)); + const locale = useSelector((state) => getLocale(state)); + const routes = useSelector((state) => getRoutes(state)); + const { theme } = useContext(ThemeContext); - renderContent() { - const { children, currentRoute, toggleLocale, locale, routes } = this.props; - const { navigation } = routes; + const prevPath = useRef(currentRoute.path); - const mappingFuncs = { - 'en-xa': translateUsingPseudoLocale, - }; + useEffect(() => { + const onKeydown = (event) => { + if (event.target !== document.body) { + return; + } - const i18n = { - mappingFunc: mappingFuncs[locale], - formatNumber: (value) => new Intl.NumberFormat(locale).format(value), - locale, - }; + if (event.metaKey) { + return; + } - const isLocalDev = window.location.host.includes('803'); - - return ( - <> - - {`${this.props.currentRoute.name} - Elastic UI Framework`} - - - - - - - - - - - - - - {(context) => { - return React.cloneElement(children, { - selectedTheme: context.theme, - title: currentRoute.name, - }); - }} - - - - - - ); - } - - render() { - return this.renderContent(); - } - - onKeydown = (event) => { - if (event.target !== document.body) { - return; - } + if (event.key === keys.ARROW_LEFT) { + pushRoute(routes.getPreviousRoute); + return; + } - if (event.metaKey) { - return; - } + if (event.key === keys.ARROW_RIGHT) { + pushRoute(routes.getNextRoute); + } - const { routes, currentRoute } = this.props; + function pushRoute(getRoute) { + const route = getRoute(currentRoute.name); - if (event.key === keys.ARROW_LEFT) { - pushRoute(routes.getPreviousRoute); - return; - } + if (route) { + routes.history.push(`/${route.path}`); + } + } + }; - if (event.key === keys.ARROW_RIGHT) { - pushRoute(routes.getNextRoute); - } + document.addEventListener('keydown', onKeydown); - function pushRoute(getRoute) { - const route = getRoute(currentRoute.name); + return () => { + document.removeEventListener('keydown', onKeydown); + }; + }, []); // eslint-disable-line - if (route) { - routes.history.push(`/${route.path}`); - } + useEffect(() => { + if (prevPath.current !== currentRoute.path) { + window.scrollTo(0, 0); + prevPath.current = currentRoute.path; } - }; -} + }, [currentRoute.path]); + + return ( + + + + + + + + {children({ theme })} + + + ); +}; AppView.propTypes = { children: PropTypes.any, currentRoute: PropTypes.object.isRequired, - locale: PropTypes.string.isRequired, - toggleLocale: PropTypes.func.isRequired, - routes: PropTypes.object.isRequired, }; AppView.defaultProps = { diff --git a/src-docs/src/views/guidelines/getting_started.md b/src-docs/src/views/guidelines/getting_started.md deleted file mode 100644 index b9d9a04b7f3..00000000000 --- a/src-docs/src/views/guidelines/getting_started.md +++ /dev/null @@ -1,259 +0,0 @@ -## Installation - -To install the Elastic UI Framework into an existing project, use the `yarn` CLI (`npm` is not supported). - -``` -yarn add @elastic/eui -``` - -Note that EUI has [several `peerDependencies` requirements](https://github.com/elastic/eui/package.json) that will also need to be installed if starting with a blank project. You can read more about other ways to [consume EUI](https://github.com/elastic/eui/blob/main/wiki/consuming.md). - -```js -yarn add @elastic/eui @elastic/datemath moment prop-types -``` - - -## Running Locally - -### Node - -We depend upon the version of node defined in [.nvmrc](https://github.com/elastic/eui/.nvmrc). - -You will probably want to install a node version manager. [nvm](https://github.com/creationix/nvm) is recommended. - -To install and use the correct node version with `nvm`: - -```js -nvm install -``` - -### Documentation - -You can run the documentation locally at `http://localhost:8030/` by running the following. - -```js -yarn -yarn start -``` - -If another process is already listening on port 8030, the next free port will be used. Alternatively, you can specify a port: - -```js -yarn start --port 9000 -``` -## Requirements and dependencies - -EUI expects that you polyfill ES2015 features, e.g. [`babel-polyfill`](https://babeljs.io/docs/usage/polyfill/). Without an ES2015 polyfill your app might throw errors on certain browsers. - -EUI also has `moment` and `@elastic/datemath` as dependencies itself. These are already loaded in most Elastic repos, but make sure to install them if you are starting from scratch. - -## What's available - -EUI publishes React UI components, JavaScript helpers called services, and utilities for writing Jest tests. Please refer to the [Elastic UI Framework website](https://elastic.github.io/eui) for comprehensive info on what's available. - -EUI is published through [NPM](https://www.npmjs.com/package/@elastic/eui) as a dependency. We also provide a starter projects for: -- [GatsbyJS](https://github.com/elastic/gatsby-eui-starter) -- [NextJS](https://github.com/elastic/next-eui-starter) - -### Components - -You can import React components from the top-level EUI module. - -```js -import { - EuiButton, - EuiCallOut, - EuiPanel, -} from '@elastic/eui'; -``` - -### Services - -Most services are published from the `lib/services` directory. Some are published from their module directories in this directory. - -```js -import { keys } from '@elastic/eui/lib/services'; -import { Timer } from '@elastic/eui/lib/services/time'; -``` - -### Test - -Test utilities are published from the `lib/test` directory. - -```js -import { findTestSubject } from '@elastic/eui/lib/test'; -``` - -## Using EUI in a standalone project - -You can consume EUI in standalone projects, such as plugins and prototypes. - -### Importing compiled CSS - -Most of the time, you just need the compiled CSS, which provides the styling for the React components. - -```js -import '@elastic/eui/dist/eui_theme_light.css'; -``` - -Other compiled themes include: -```js -import '@elastic/eui/dist/eui_theme_dark.css'; -``` -```js -import '@elastic/eui/dist/eui_theme_amsterdam_light.css'; -``` -```js -import '@elastic/eui/dist/eui_theme_amsterdam_dark.css'; -``` - -### Using our Sass variables on top of compiled CSS - -If you want to build **on top** of the EUI theme by accessing the Sass variables, functions, and mixins, you'll need to import the Sass globals in addition to the compiled CSS mentioned above. This will require `style`, `css`, `postcss`, and `sass` loaders. - -First import the correct colors file, followed by the globals file. - -```scss -@import '@elastic/eui/src/themes/eui/eui_colors_light.scss'; -@import '@elastic/eui/src/themes/eui/eui_globals.scss'; -``` - -For the dark theme, swap the first import for the dark colors file. - -```scss -@import '@elastic/eui/src/themes/eui/eui_colors_dark.scss'; -@import '@elastic/eui/src/themes/eui/eui_globals.scss'; -``` - -If you want to use the new, but in progress Amsterdam theme, you can import it similarly. - -```scss -@import '@elastic/eui/src/themes/eui-amsterdam/eui_amsterdam_colors_light.scss'; -@import '@elastic/eui/src/themes/eui-amsterdam/eui_amsterdam_globals.scss'; -``` - -### Using Sass to customize EUI - -EUI's Sass themes are token based, which can be altered to suite your theming needs like changing the primary color. Simply declare your token overrides before importing the whole EUI theme. This will re-compile **all of the EUI components** with your colors. - -*Do not use in conjunction with the compiled CSS.* - -Here is an example setup. - -```scss -// mytheme.scss -$euiColorPrimary: #7B61FF; - -@import '@elastic/eui/src/themes/eui/eui_colors_light.scss'; -@import '@elastic/eui/src/themes/eui/eui_globals.scss'; -``` - -### Fonts - -By default, EUI ships with a font stack that includes some outside, open source fonts. If your system is internet available you can include these by adding the following imports to your SCSS/CSS files, otherwise you'll need to bundle the physical fonts in your build. EUI will drop to System Fonts (which you may prefer) in their absence. - -```scss -@import url('https://fonts.googleapis.com/css?family=Roboto+Mono:400,400i,700,700i'); -@import url('https://rsms.me/inter/inter-ui.css'); -``` - -The Amsterdam theme uses the latest version of Inter that can be grabbed from Google Fonts as well. - -```scss -@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=Roboto+Mono:ital,wght@0,400;0,700;1,400;1,700&display=swap'); -``` - -Or grab all weights, including italics, between 400 and 700 as a variable font. - -```scss -@import url('https://fonts.googleapis.com/css2?family=Inter:slnt,wght@-10,300..700;0,300..700&family=Roboto+Mono:ital,wght@0,400..700;1,400..700&display=swap'); -``` - -#### Embedding with `@font-face` - -If your application doesn't allow grabbing the font assets from a CDN, you'll need to download and locally provide the font files. You should download the files directly from their source site [rsms.me/inter/](https://rsms.me/inter/). Then follow the instructions in the provided CSS file for importing. We recommend using the single variable font file and importing with the following settings: - -```scss -@font-face { - font-family: 'Inter'; - font-weight: 300 900; - font-display: swap; - font-style: oblique 0deg 10deg; - src: url("Inter.var.woff2") format("woff2"); -} -``` - -### Reusing the variables in JavaScript - -The Sass variables are also made available for consumption as json files. This enables reuse of values in css-in-js systems like [styled-components](https://www.styled-components.com). As the following example shows, it can also make the downstream components theme-aware without much extra effort: - -```js -import * as React from 'react'; -import * as ReactDOM from 'react-dom'; -import styled, { ThemeProvider } from 'styled-components'; -import * as euiVars from '@elastic/eui/dist/eui_theme_light.json'; - -const CustomComponent = styled.div` - color: ${props => props.theme.euiColorPrimary}; - border: ${props => props.theme.euiBorderThin}; -`; - -ReactDOM.render( - - content - -, document.querySelector('#renderTarget')); -``` - -### "Module build failed" or "Module parse failed: Unexpected token" error - -If you get an error when importing a React component, you might need to configure Webpack's `resolve.mainFields` to `['webpack', 'browser', 'main']` to import the components from `lib` instead of `src`. See the [Webpack docs](https://webpack.js.org/configuration/resolve/#resolve-mainfields) for more info. - -### Failing icon imports - -To reduce EUI's impact to application bundle sizes, the icons are dynamically imported on-demand. This is problematic for some bundlers and/or deployments, so a method exists to preload specific icons an application needs. - -```javascript -import { appendIconComponentCache } from '@elastic/eui/es/components/icon/icon'; - -import { icon as EuiIconArrowDown } from '@elastic/eui/es/components/icon/assets/arrow_down'; -import { icon as EuiIconArrowLeft } from '@elastic/eui/es/components/icon/assets/arrow_left'; - -// One or more icons are passed in as an object of iconKey (string): IconComponent -appendIconComponentCache({ - arrowDown: EuiIconArrowDown, - arrowLeft: EuiIconArrowLeft, -}); -``` - -## Customizing with custom classes - -We do not recommend customizing EUI components by applying styles directly to EUI classes, eg. `.euiButton`. All components allow you to pass a custom `className` prop directly to the component which will then append this to the class list. Utilizing the cascade feature of CSS, you can then customize by overriding styles so long as your styles are imported **after** the EUI import. - -```html - - -// Renders as: - -