React
Modes UI offers a React version of every component to provide an idiomatic experience for React users. You can easily toggle between HTML and React examples throughout the documentation.
If your event listeners are causing unintended side-effects, see the section on event bubbling
Installation
To add Modes UI to your React app, install the package from npm by following the instructions on the installation page.
Next, include a theme and set the base path for icons and other assets. Setting the base path is important on React, as Modes UI cannot automatically find the path. In this example, we’ll import the light theme and set the base path.
// App.jsx import '@metsooutotec/modes-web-components/dist/themes/light.css'; import { setBasePath } from '@metsooutotec/modes-web-components'; // E.g., set base path to the public directory, see info below setBasePath(process.env.PUBLIC_URL);
You can create a
build task
that copies node_modules/@metsooutotec/modes-web-components/dist/assets
into your app’s
public
directory. Then you can point the base path to that folder as shown. See
common errors for more information.
Now you can start using components!
Setting up Vite
The SVG icons are handled slightly differently if you are using Vite.
Install packages vite-plugin-static-copy
and vite-plugin-svgr
. Then add the
following configuration to your vite.config.ts
:
import react from '@vitejs/plugin-react'; import path from 'path'; import { defineConfig, normalizePath } from 'vite'; import { viteStaticCopy } from 'vite-plugin-static-copy'; import svgr from 'vite-plugin-svgr'; // https://vitejs.dev/config/ export default defineConfig({ plugins: [ react(), svgr(), viteStaticCopy({ targets: [ { src: normalizePath( path.resolve(__dirname, './node_modules/@metsooutotec/modes-web-components/dist/assets/icons') ), dest: './assets' } ] }) ] });
Use the following code to set the base path:
import { setBasePath } from "@metsooutotec/modes-web-components"; import "@metsooutotec/modes-web-components/dist/themes/dark.css"; import "@metsooutotec/modes-web-components/dist/themes/light.css"; import React from "react"; import ReactDOM from "react-dom/client"; import App from "./App.tsx"; import "./index.css"; setBasePath("/"); ReactDOM.createRoot(document.getElementById("root")!).render( <React.StrictMode> <App /> </React.StrictMode> );
Usage
Importing components
Every Modes UI component is available to import as a React component. Note that we’re importing the
<MOButton>
React component instead of the <mo-button>
custom element in the example below.
import MOButton from '@metsooutotec/modes-web-components/dist/react/button'; const MyComponent = () => <MOButton variant="primary">Click me</MOButton>; export default MyComponent;
You can find a copy + paste import for each component in the “importing” section of its documentation.
It is possible to cherry-pick React components by importing them individually from
@metsooutotec/modes-web-components/dist/react/button
for example. This method
(“cherry-picking”) is recommended in order to
minimize the bundle size. Alternatively you can import the React components from the index:
import { MOInput } from '@metsooutotec/modes-web-components/dist/react';
, however this may
result in a larger bundle size.
Event handling
Many Modes UI components emit
custom events. For example, the input component emits the mo-input
event
when it receives input. In React, you can listen for the event using onMoInput
.
Here’s how you can bind the input’s value to a state variable.
import { useState } from 'react'; import { MOInput } from '@metsooutotec/modes-web-components/dist/react'; function MyComponent() { const [value, setValue] = useState(''); return <MOInput value={value} onMoInput={event => setValue(event.target.value)} />; } export default MyComponent;
If you’re using TypeScript, it’s important to note that event.target
will be a reference to the
underlying custom element. You can use (event.target as any).value
as a quick fix, or you can
strongly type the event target as shown below.
import { useState } from 'react'; import { MOInput } from '@metsooutotec/modes-web-components/dist/react'; import type MOInputElement from '@metsooutotec/modes-web-components/dist/components/input/input'; function MyComponent() { const [value, setValue] = useState(''); return <MOInput value={value} onMoInput={event => setValue((event.target as MOInputElement).value)} />; } export default MyComponent;
You can also import the event type for use in your callbacks, shown below.
import { useCallback, useState } from 'react'; import { MOInput, MOInputEvent } from '@metsooutotec/modes-web-components/dist/react'; import type MOInputElement from '@metsooutotec/modes-web-components/dist/components/input/input'; function MyComponent() { const [value, setValue] = useState(''); const onInput = useCallback((event: MOInputEvent) => { setValue(event.detail); }, []); return <MOInput value={value} onMOInput={event => setValue((event.target as MOInputElement).value)} />; } export default MyComponent;
If your event listeners are causing unintended side-effects, see the section on event bubbling.
Theming
Importing the css
Modes UI’s components need theme files to properly show in correct colors and styling. These theme files can be imported through module imports, or inside another .css/.scss file, much like the DSUI style files.
Modules (.jsx/.tsx files):
import '@metsooutotec/modes-web-components/dist/themes/light.css'; /* Import dark.css if you want to have dark mode support **/ import '@metsooutotec/modes-web-components/dist/themes/dark.css';
CSS (.css/.scss files):
@import '~@metsooutotec/modes-web-components/dist/themes/light.css'; /* Import dark.css if you want to have dark mode support **/ @import '~@metsooutotec/modes-web-components/dist/themes/dark.css';
Handling dark mode
In order for Modes UI components to automatically shift to dark mode, it requires the DOM root tag to
contain the class mo-dark-theme
. One example of doing this is using React’s useEffect to update
the DOM as the theme
dependency changes:
enum Themes { DARK = 'dark', LIGHT = 'light', } useEffect(() => { const mainHtml = document.getElementsByTagName('html')[0] const updateModesTheme = (theme: Themes) => { // toggle for theme switching in Modes UI if (theme !== Themes.DARK) { mainHtml.classList.add('mo-theme-light') mainHtml.classList.remove('mo-theme-dark') } else { mainHtml.classList.add('mo-theme-dark') mainHtml.classList.remove('mo-theme-light') } }; updateModesTheme(theme); return () => { // 👇️ removing classes from body element // when the component unmounts mainHtml.classList.remove('mo-theme-dark'); mainHtml.classList.remove('mo-theme-light'); }; }, [theme]);
Alternatively adding the class to the body can be done e.g., inside the onClick
function of a
button/switch. This is just one example.
The light mode works without the class name, as it is applied to the :root
, but the dark
theme requires the class name to activate.
Testing with Jest
Testing with web components can be challenging if your test environment runs in a Node environment (i.e. it doesn’t run in a real browser). Fortunately, Jest has made a number of strides to support web components and provide additional browser APIs. However, it’s still not a complete replication of a browser environment.
Here are some tips that will help smooth things over if you’re having trouble with Jest + Modes UI.
If you’re looking for a fast, modern testing alternative, consider Web Test Runner.
Upgrade Jest
Jest underwent a major revamp and received support for web components in
version 26.5.0
when it introduced
JSDOM 16.2.0. This release also included a number of mocks for built-in browser functions such as
MutationObserver
, document.createRange
, and others.
If you’re using
Create React App, you can update react-scripts
which will also update Jest.
npm install react-scripts@latest
Mock missing APIs
Some components use window.matchMedia
, but this function isn’t supported by JSDOM so you’ll
need to mock it yourself.
In src/setupTests.js
, add the following.
Object.defineProperty(window, 'matchMedia', { writable: true, value: jest.fn().mockImplementation(query => ({ matches: false, media: query, onchange: null, addListener: jest.fn(), // deprecated removeListener: jest.fn(), // deprecated addEventListener: jest.fn(), removeEventListener: jest.fn(), dispatchEvent: jest.fn() })) });
For more details, refer to Jest’s manual mocking documentation.
Transform ES Modules
ES Modules are a well-supported browser standard. This is how Modes UI is distributed, but most React apps expect CommonJS. As a result, you’ll probably run into the following error.
Error: Unable to import outside of a module
To fix this, add the following to your package.json
which tells the transpiler to process Modes
UI modules.
{ "jest": { "transformIgnorePatterns": ["node_modules/(?!(@metsooutotec/modes-web-components)/)"] } }
You may need to install babel-jest
and
configure it as well.
These instructions are for apps created via Create React App. If you’re using Jest directly, you can add
transformIgnorePatterns
directly into jest.config.js
.
For more details, refer to Jest’s
transformIgnorePatterns
customization
documentation.