Skip to main content
Default Gray Amethyst

Migration guide

The migration guide includes instructions on how to migrate to the latest major version, with detailed descriptions of the breaking changes that come with the release. See the changelog for all the changes.

v4.0 (July 3, 2024)

Version 4.0 should not have significant breaking changes for most users. The main major changes are the removal of LightningCharts, value setting in Combobox and font adjustments. 4.0 introduces two new components, mo-context-menu and mo-kbd. These components are often used together with the improved mo-menu that now has functionality for including submenus.

The documentation tool for Modes UI has been changed from Docsify to Eleventy, and the design has received a major facelift. A new documentation template repository has been established and templates are now possible to add to the Modes UI documentation.

Breaking changes

LightningChart based components have been removed. Combobox received an overhaul in the way it handles programmatic value setting, which may break existing behavior in projects. The GT-Eesti font is now defined slightly differently. Find more information below.

Dependencies

There are a number of dependency upgrades and changes in this version, but few are relevant to end-users of the library. Many devDependencies have been upgraded or changed, see below for the most important changes, and #221 for all of the changes.

LightningCharts have been separated into their own library. See below for more information.

  • - ”@arction/lcjs”: ”^5.2.0″
  • - ”@arction/lcjs-themes”: ”^3.2.1″

The react wrapping utility library from Lit has been migrated to using the new stable release

  • - ”@lit-labs/react”: ”^1.0.2″
  • + ”@lit/react”: ”^1.0.0″

Bundling

The library package is now available in two different options. The previously familiar /dist folder no longer includes dependencies such as Lit bundled into it. This is to ensure users who have overlapping dependencies in their own project are able to only include one copy of them. This however means that the library in /dist will not work unbundled.

+ /cdn

If you wish to include fetch the library with all dependencies bundled into it, use the new /cdn folder. They contain the same exact set of components, styles, etc., but /cdn has the dependencies of Modes UI bundled into it. This is used in the new modes-docs-template for example, as the library is just imported with a simple module script in HTML.

Using "moduleResolution: "Node"

/** React wrapped component */
import MOButton from '@metsooutotec/modes-web-components/dist/react/button';
/** Just the web component */
import '@metsooutotec/modes-web-components/dist/components/button/button';

Using "moduleResolution: "Bundler"

/** React wrapped component */
import MOButton from '@metsooutotec/modes-web-components/dist/react/button/index.js';
/** Just the web component */
import '@metsooutotec/modes-web-components/dist/components/button/button.js';

Events

Version 4.0 exports Typescript types for all Modes UI events. Event types can be imported either individually as a default export from each event type file, or in bulk from the events.ts file which re-exports all events in a single file.

This removes the need for consumers using TypeScript to redefine the events, and ensures that any breaking changes in Modes UI events will be detected at transpile time by consumers.

Emitted events inside components are now more consistent, before some mo-change events had a detail, and the detail had mixed contents. Now all mo-change events have no detail object attached to them, and some use cases have been refactored in to new events. See all the component specific event changes below.

Breaking changes

  • Chip no longer emits mo-select when selected, now emits mo-change
  • File dropzone no longer emits a detail alongside mo-abort, mo-remove, and mo-change events
    • some are replaced by new mo-transfer-abort, mo-transfer-error, mo-transfer-load events that still contain the payload in the detail, see the new event files for details
  • Pagination now fires mo-page-change event instead of mo-change event, detail remains the same
  • Rich text editor now emits a new mo-editor-update event when the Editor has finished updating (instead of a detailed mo-change)
  • The deprecated Toggle button no longer emits a detail alongside the mo-change event
  • Time picker now emits a new mo-time-change event instead of the mo-change event

You can also now import the event type for use in your callbacks, as 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;

Combobox

The mo-combobox received a major refactoring in the way it handles its internal value (#192). Below is a list of the changes made

Breaking Changes

  • The mo-combobox now aligns with mo-select by using a space-delimited string as an attribute and an array of strings as a property when multiple selections are enabled.
  • The initial and programmatic selection for mo-combobox is now managed solely through the value property.
  • The hierarchical-content attribute has been removed from mo-combobox as it is no longer necessary.
  • The visibleValue is no longer a public property on mo-combobox; instead, use the value property to control the selection.
  • The mo-tree-item component (used internally within combobox) no longer emits mo-select and mo-deselect events. The mo-selection-change event from mo-tree is now the only event emitted from the tree.
  • The mo-combobox no longer automatically closes on value selection, use the new close-on-selection attribute to control this behavior

New Features and Fixes

  • The mo-combobox now matches results that start with the query, rather than just checking if they include the query as a substring.
  • The mo-combobox now highlights the non-queried part of the results instead of the query itself.
  • A public reset() function has been added to mo-combobox that properly resets the combobox to an empty state.
  • Internal functions of mo-combobox are now properly marked as private, with JSDoc added to public functions.
  • A virtualization regression bug in mo-combobox has been fixed, ensuring that new items load correctly when filtering the tree items.
  • The simple mo-combobox now correctly sets an initial selection based on the value attribute matching one of the items in the options object.
  • Programmatically changing the value attribute now selects the nodes with the matching IDs, and in cases of multiple selections, values are separated by spaces, similar to mo-select.
selected nodes:
<div>
  <mo-combobox
    style="flex: 1 1 auto;"
    label="Celestial objects"
    placeholder="Select variable"
    id="tree-controlled"
    help-text="Choose one option"
    clearable
  ></mo-combobox>
  <mo-input
    help-text="Input node id to be selected here."
    id="node-id-input"
    placeholder="Example: 2-3-1-2"
    label="value"
  ></mo-input>
</div>
<mo-divider></mo-divider>
<div>
  <mo-combobox
    style="flex: 1 1 auto;"
    label="Celestial objects"
    placeholder="Select variable"
    id="tree-controlled-multiple"
    help-text="Multiple options can be selected"
    value="2-1-1 1-1-2"
    selection-mode="multiple"
    clearable
  ></mo-combobox>
  <mo-input
    id="node-id-input-multiple"
    value="2-1-1 1-1-2"
    help-text="Input node id to be selected here."
    placeholder="Example: 2-1-1 1-1-2"
    label="value"
  ></mo-input>
</div>
<pre id="pre">selected nodes:</pre>

<script>
  const treeBox = document.querySelector('#tree-controlled');
  const treeBoxMulti = document.querySelector('#tree-controlled-multiple');
  const input = document.querySelector('#node-id-input');
  const inputMulti = document.querySelector('#node-id-input-multiple');
  const pre = document.querySelector('#pre');
  const treeItems = [
    {
      name: 'Galaxies',
      id: '1',
      children: [
        {
          name: 'Elliptical',
          id: '1-1',
          children: [
            { name: 'IC 1101', id: '1-1-1' },
            { name: 'Hercules A', id: '1-1-2', selected: true },
            { name: 'A2261-BCG', id: '1-1-3' },
            { name: 'ESO 306-17', id: '1-1-4' },
            { name: 'ESO 444-46', id: '1-1-5' }
          ]
        },
        {
          name: 'Spiral',
          id: '1-2',
          children: [
            { name: "Rubin's Galaxy", id: '1-2-1' },
            { name: 'Comet Galaxy', id: '1-2-2' },
            { name: 'Condor Galaxy', id: '1-2-3' },
            { name: 'Tadpole Galaxy', id: '1-2-4' },
            { name: 'Andromeda Galaxy', id: '1-2-5' }
          ]
        }
      ]
    },
    {
      name: 'Planets',
      id: '2',
      children: [
        {
          name: 'Sub-Earth',
          id: '2-1',
          children: [
            { name: 'Mars', id: '2-1-1' },
            { name: 'Mercury', id: '2-1-2' }
          ]
        },
        {
          name: 'Giant',
          id: '2-2',
          children: [
            { name: 'Jupiter', id: '2-2-1' },
            { name: 'Saturn', id: '2-2-2' },
            { name: 'Uranus', id: '2-2-3' },
            { name: 'Neptune', id: '2-2-4' }
          ]
        },
        {
          name: 'Exoplanet',
          id: '2-3',
          children: [
            {
              name: 'Potentially habitable',
              id: '2-3-1',
              children: [
                { name: 'Alpha Centauri', id: '2-3-1-1' },
                { name: 'Ross 128', id: '2-3-1-2' },
                { name: 'Wolf 1061', id: '2-3-1-3' }
              ]
            },
            { name: 'Epsilon Eridani', id: '2-3-2' },
            { name: 'YZ Ceti', id: '2-3-4' }
          ]
        }
      ]
    }
  ];

  treeBox.treeItems = treeItems;
  treeBoxMulti.treeItems = treeItems;
  treeBoxMulti.addEventListener('mo-selection-change', e => {
    pre.textContent = 'selected nodes: ' + treeBoxMulti.value;
  });
  input.addEventListener('mo-input', () => {
    treeBox.value = input.value;
  });
  inputMulti.addEventListener('mo-input', () => {
    treeBoxMulti.value = inputMulti.value;
  });
</script>
import { MOCombobox, MODivider, MOInput, MOButton } from '@metsooutotec/modes-web-components/dist/react';

const treeItems = [
  {
    name: 'Galaxies',
    id: '1',
    children: [
      {
        name: 'Elliptical',
        id: '1-1',
        children: [
          { name: 'IC 1101', id: '1-1-1' },
          { name: 'Hercules A', id: '1-1-2' },
          { name: 'A2261-BCG', id: '1-1-3' },
          { name: 'ESO 306-17', id: '1-1-4' },
          { name: 'ESO 444-46', id: '1-1-5' }
        ]
      },
      {
        name: 'Spiral',
        id: '1-2',
        children: [
          { name: "Rubin's Galaxy", id: '1-2-1' },
          { name: 'Comet Galaxy', id: '1-2-2' },
          { name: 'Condor Galaxy', id: '1-2-3' },
          { name: 'Tadpole Galaxy', id: '1-2-4' },
          { name: 'Andromeda Galaxy', id: '1-2-5' }
        ]
      }
    ]
  },
  {
    name: 'Planets',
    id: '2',
    children: [
      {
        name: 'Sub-Earth',
        id: '2-1',
        children: [
          { name: 'Mars', id: '2-1-1' },
          { name: 'Mercury', id: '2-1-2' }
        ]
      },
      {
        name: 'Giant',
        id: '2-2',
        children: [
          { name: 'Jupiter', id: '2-2-1' },
          { name: 'Saturn', id: '2-2-2' },
          { name: 'Uranus', id: '2-2-3' },
          { name: 'Neptune', id: '2-2-4' }
        ]
      },
      {
        name: 'Exoplanet',
        id: '2-3',
        children: [
          {
            name: 'Potentially habitable',
            id: '2-3-1',
            children: [
              { name: 'Alpha Centauri', id: '2-3-1-1' },
              { name: 'Ross 128', id: '2-3-1-2' },
              { name: 'Wolf 1061', id: '2-3-1-3' }
            ]
          },
          { name: 'Epsilon Eridani', id: '2-3-2' },
          { name: 'YZ Ceti', id: '2-3-4' }
        ]
      }
    ]
  }
];

const inputRef = useRef(null);
const cbRef = useRef(null);

const setNode = () => {
  cbRef.current.selectNode(cbRef.current.getNodeById(inputRef.value));
};

const App = () => (
  <>
    <MOCombobox
      label="Celestial objects"
      placeholder="Select variable"
      clearable
      ref={cbRef}
      treeItems={treeItems}
    ></MOCombobox>
    <MODivider></MODivider>
    <MOInput ref={inputRef} placeholder="Example: 2-3-1-2" label="Node id"></MOInput>
    <br />
    <MOButton onClick={setNode}>Select node</MOButton>
  </>
);

LightningChart components

Data visualization components based on LightningCharts have been removed from Modes UI and they are now their own standalone library in Modes LC components. Anyone with a Metso GitHub license will be able to access the new library, but the project must ensure they have proper licensing before beginning usage. The new library has a demo site hosted in a separate static web application:

Tree

The mo-tree-item component (used internally within combobox) no longer emits mo-select and mo-deselect events. The mo-selection-change event from mo-tree is now the only event emitted from the tree.

Font changes

Previously Modes UI had two separate font-families and tokens defined for the Light and Regular variants of GT-Eesti. Since 4.0, these two variants will be combined in to just one font: var(--mo-font-sans) will now contain both of these and the Regular (Heading style) can be accessed by setting the font-weight to either bold or 700.

All components will use the new syntax starting from 4.0, but if you previously used font-family: GT-Eesti-Light or var(--mo-font-sans-regular) in your project for your custom styling, you will have to modify these to fit the new logic:

/** Deprecated way (<= 3.2.0) */
.header {
  /** Deprecated token */
  font-family: var(--mo-font-sans-regular);
  /** No longer included in the library with this name */
  font-family: 'GT-Eesti-Light';
}

/** New way (>= v4.0) */
.header {
  /** Contains both GT-Eesti-Light and GT-Eesti-Regular */
  font-family: var(--mo-font-sans);
  /** The only font-family included in the library since 4.0 */
  font-family: 'GT-Eesti'

  /** Set font-weight to 700 to use GT-Eesti-Regular */
  font-weight: var(--mo-font-weight-bold);
}

New components

Version 4.0 brings a couple simple components that open up new ways to use the mo-menu within your application.

Context menu

The new mo-context-menu components allows you to customize the browser default context menu when the user clicks within a trigger area that you have specified.

Right click in this area to trigger a context menu
Copy Paste Cut Disabled Show toolbar Find Find… Find previous Find next Transformations Make uppercase Make lowercase Capitalize
<mo-context-menu closeOnSelection>
  <div class="trigger-area" slot="trigger">
    Right click in this area to trigger a context menu
  </div>
  <mo-menu density="compact">
    <mo-menu-item>Copy</mo-menu-item>
    <mo-menu-item>Paste</mo-menu-item>
    <mo-menu-item>Cut</mo-menu-item>
    <mo-menu-item disabled value="disabled">Disabled</mo-menu-item>
    <mo-divider></mo-divider>
    <mo-menu-item type="checkbox" checked value="copy">Show toolbar</mo-menu-item>
    <mo-divider></mo-divider>
    <mo-menu-item>
      Find
      <mo-icon slot="prefix" name="search"></mo-icon>
      <mo-menu density="compact" slot="submenu">
        <mo-menu-item value="find">Find…</mo-menu-item>
        <mo-menu-item value="find-previous">Find previous</mo-menu-item>
        <mo-menu-item value="find-next">Find next</mo-menu-item>
      </mo-menu>
    </mo-menu-item>
    <mo-menu-item>
      Transformations
      <mo-icon slot="prefix" name="text-style"></mo-icon>
      <mo-menu density="compact" slot="submenu">
        <mo-menu-item value="uppercase">Make uppercase</mo-menu-item>
        <mo-menu-item value="lowercase">Make lowercase</mo-menu-item>
        <mo-menu-item value="capitalize">Capitalize</mo-menu-item>
      </mo-menu>
    </mo-menu-item>
  </mo-menu>
</mo-context-menu>

<style>
  .trigger-area {
    border: 1px dashed var(--mo-color-neutral-70);
    padding: 4rem 2rem;
    text-align: center;
  }
</style>
import { MOMenu, MOMenuItem, MODivider, MOIcon, MOContextMenu } from '@metsooutotec/modes-web-components/dist/react';

const App = () => (
  <MOContextMenu closeOnSelection>
    <div className="trigger-area" slot="trigger">
      Right click in this area to trigger a context menu
    </div>
    <MOMenu density="compact">
      <MOMenuItem>Copy</MOMenuItem>
      <MOMenuItem>Paste</MOMenuItem>
      <MOMenuItem>Cut</MOMenuItem>
      <MOMenuItem disabled value="disabled">Disabled</MOMenuItem>
      <MODivider></MODivider>
      <MOMenuItem type="checkbox" checked value="copy">Show toolbar</MOMenuItem>
      <MODivider></MODivider>
      <MOMenuItem>
        Find
        <MOIcon slot="prefix" name="search"></MOIcon>
        <MOMenu density="compact" slot="submenu">
          <MOMenuItem value="find">Find…</MOMenuItem>
          <MOMenuItem value="find-previous">Find previous</MOMenuItem>
          <MOMenuItem value="find-next">Find next</MOMenuItem>
        </MOMenu>
      </MOMenuItem>
      <MOMenuItem>
        Transformations
        <MOIcon slot="prefix" name="text-style"></MOIcon>
        <MOMenu density="compact" slot="submenu">
          <MOMenuItem value="uppercase">Make uppercase</MOMenuItem>
          <MOMenuItem value="lowercase">Make lowercase</MOMenuItem>
          <MOMenuItem value="capitalize">Capitalize</MOMenuItem>
        </MOMenu>
      </MOMenuItem>
    </MOMenu>
  </MOContextMenu>
)

Kbd

The mo-kbd is a simple utility that allows you to visualize keyboard shortcuts for your users easily and consistently. These shortcuts are typically shown within menus or inputs.

Copy C Paste V Cut X
<mo-menu class="kbd-menu" style="max-width: 200px;">
  <mo-menu-item>
    Copy
    <mo-kbd size="small" slot="suffix" keys="ctrl">C</mo-kbd>
  </mo-menu-item>
  <mo-menu-item>
    Paste
    <mo-kbd size="small" slot="suffix" keys="ctrl">V</mo-kbd>
  </mo-menu-item>
  <mo-menu-item>
    Cut
    <mo-kbd size="small" slot="suffix" keys="ctrl">X</mo-kbd>
  </mo-menu-item>
</mo-menu>

<style>
  .kbd-menu mo-menu-item::part(checked-icon) {
    width: 0.25em;
  }

  .kbd-menu mo-menu-item::part(submenu-icon) {
    width: 0em;
  }
</style>
import { MOKbd, MOMenu, MOMenuItem } from '@metsooutotec/modes-web-components/dist/react';

const App = () => (
  <MOMenu class="kbd-menu" style="max-width: 200px;">
    <MOMenuItem>
      Copy
      <MOKbd size="small" slot="suffix" keys="ctrl">C</MOKbd>
    </MOMenuItem>
    <MOMenuItem>
      Paste
      <MOKbd size="small" slot="suffix" keys="ctrl">V</MOKbd>
    </MOMenuItem>
    <MOMenuItem>
      Cut
      <MOKbd size="small" slot="suffix" keys="ctrl">X</MOKbd>
    </MOMenuItem>
  </MOMenu>
);

New features

Version 4.0 also includes some non-breaking feature updates to some existing components.

Zooming and panning in data visualization

The mo-line-chart and mo-scatter-plot zooming functionality has been improved by default and it’s now more customizable. Highlighting an area to zoom is now the default and panning can be enabled by holding down the (Control) button. You can customize this behavior using the new zoomOptions attribute.

Reset zoom
<mo-scatter-plot
  yAxisLabel="Vertical displacement"
  xAxisLabel="Horizontal displacement"
  yAxisUnit="mm"
  xAxisUnit="mm"
  title="Scatter plot"
  subtitle="With zoom & pan"
  zoomable
  id="zooming"
></mo-scatter-plot>

<mo-button id="reset-zoom-btn">Reset zoom</mo-button>

<script>
  const chart = document.querySelector('#zooming');
  const resetBtn = document.querySelector('#reset-zoom-btn');
  const dataOne = [
    {
      x: 1.44,
      y: 9.522
    },
    {
      x: 8.953,
      y: 5.912
    },
    {
      x: 0.533221,
      y: 5.53
    },
    {
      x: 3.5,
      y: 7.35
    },
    {
      x: 6.47,
      y: 4.98
    },
    {
      x: 7.723,
      y: 5.91
    },
    {
      x: 5.821123,
      y: 6.83
    }
  ];

  const dataTwo = [
    {
      x: 1.12,
      y: 5.7
    },
    {
      x: 7.5222,
      y: 4.91
    },
    {
      x: 1.55,
      y: 4.4
    },
    {
      x: 4.12,
      y: 6.911
    },
    {
      x: 6.15,
      y: 4.424
    },
    {
      x: 7.1,
      y: 5.421
    },
    {
      x: 6,
      y: 7.152
    }
  ];

  const dataThree = [
    {
      x: 3.123,
      y: 3.17
    },
    {
      x: 2.422,
      y: 4.11453
    },
    {
      x: 1.1,
      y: 2
    },
    {
      x: 2.8235,
      y: 2.512
    },
    {
      x: 9.2,
      y: 7.411
    },
    {
      x: 8.3,
      y: 7.453
    },
    {
      x: 4.432,
      y: 3.811
    }
  ];

  const datasets = [
    { label: 'Dataset #1', data: dataOne },
    { label: 'Dataset #2', data: dataTwo },
    { label: 'Dataset #3', data: dataThree }
  ];

  resetBtn.addEventListener('click', () => {
    chart.resetZoom();
  });

  chart.datasets = datasets;
</script>
import { MOScatterPlot } from '@metsooutotec/modes-web-components/dist/react';
import { useRef } from 'react';

const chartRef = useRef(null);

const dataOne = [
  {
    x: 1.44,
    y: 9.522
  },
  {
    x: 8.953,
    y: 5.912
  },
  {
    x: 0.533221,
    y: 5.53
  },
  {
    x: 3.5,
    y: 7.35
  },
  {
    x: 6.47,
    y: 4.98
  },
  {
    x: 7.723,
    y: 5.91
  },
  {
    x: 5.821123,
    y: 6.83
  }
];

const dataTwo = [
  {
    x: 1.12,
    y: 5.7
  },
  {
    x: 7.5222,
    y: 4.91
  },
  {
    x: 1.55,
    y: 4.4
  },
  {
    x: 4.12,
    y: 6.911
  },
  {
    x: 6.15,
    y: 4.424
  },
  {
    x: 7.1,
    y: 5.421
  },
  {
    x: 6,
    y: 7.152
  }
];

const dataThree = [
  {
    x: 3.123,
    y: 3.17
  },
  {
    x: 2.422,
    y: 4.11453
  },
  {
    x: 1.1,
    y: 2
  },
  {
    x: 2.8235,
    y: 2.512
  },
  {
    x: 9.2,
    y: 7.411
  },
  {
    x: 8.3,
    y: 7.453
  },
  {
    x: 4.432,
    y: 3.811
  }
];

const datasets = [
  { label: 'Dataset #1', data: dataOne },
  { label: 'Dataset #2', data: dataTwo },
  { label: 'Dataset #3', data: dataThree }
];

const resetZoom = () => {
  chartRef.resetZoom();
};

const App = () => (
  <>
    <MOScatterPlot
      ref={chartRef}
      yAxisLabel="Vertical displacement"
      xAxisLabel="Horizontal displacement"
      yAxisUnit="mm"
      xAxisUnit="mm"
      zoomable
      datasets={datasets}
      title="Scatter plot"
      subtitle="With point styling"
    ></MOScatterPlot>
    <MOButton onClick={resetZoom}></MOButton>
  </>
);

Programmatic values in date picker

The value attribute can now be properly set to either a valid datetime string (that follows the given format), or a Date object to set the current value programmatically. This will also work for ranges, and it will update the input and the calendars.


<mo-date-picker
  id="programmatic"
  closeOnSelection
  format="dd.MM.yyyy"
  label="Date today"
  help-text="The value has been set to today using new Date()"
></mo-date-picker>
<br />
<mo-date-picker format="dd.MM.yyyy" value="18.04.2024 - 03.07.2024" selection="range" help-text="The range has been set using a string '18.04.2024 - 03.07.2024'" label="String range"></mo-date-picker>

<script>
  const picker = document.querySelector('#programmatic');
  picker.value = new Date();
</script>
import { MODatePicker } from '@metsooutotec/modes-web-components/dist/react';

const App = () =>
  <>
    <MODatePicker
        closeOnSelection
        format="dd.MM.yyyy"
        label="Choose date"
        value={new Date()}
        helpText="Click the calendar icon to open the selector popup."
      ></MODatePicker>
    <br />
    <MODatePicker format="dd.MM.yyyy" selection="range" value="18.04.2024 - 03.07.2024" label="Choose date"></MODatePicker>
  <>;

Documentation updates

Previously Modes UI used Docsify to generate the static documentation from raw markdown files. Since 4.0 Modes UI uses Eleventy instead to accomplish the same functionality. The advantage of using Eleventy is easier configuration and future development.

Templates

As Eleventy allows for easier customization, Modes UI now includes a way to display fullscreen templates. As the initial demo for this functionality, there is a sign-in template that shows a simple way to have a branding-aligned sign in screen for users.

New documentation template

The new modes-docs-template GitHub repository contains a template for a static documentation site for Metso’s digital products. You can clone or fork the repository and customize it to fit your products needs. Its demo is hosted as a Azure Static Web App and the project contains GitHub workflows that allow this to be accomplished easily.

The template contains a lot of useful helper functions and has consistent styling with Modes UI by default. It also uses Eleventy for documentation generation, and as such documentation must be written as markdown. It also utilizes Modes UI components internally.

Other minor changes

  • introduced unit tests for mo-menu and mo-menu-item, improved accessibility
  • mo-table-head-cell sorting icon now properly takes no extra space when it is visually hidden, added example to documentation
  • a11y: mo-table now properly announces the aria-rowcount for virtualized tables and aria-selected for selected rows
  • mo-table borders are now drawn for each cell, rather than for each row
  • added additional checks to mo-donut-chart to ensure updateColors doesn’t run before the component has finished updating
  • radial gauge now properly draws indicator when the decimals attribute is defined
  • removed flag illustrations

v3.0 (April 18, 2024)

Version 3.0 of Modes UI brings a lot of new functionality and components, and some breaking changes. See the details here to ensure migration to the latest major version is as smooth as possible.

Breaking changes

This release includes breaking changes to multiple components and to some more general aspects of the library.

Dependencies

Some dependencies were removed, some were upgraded. These changes likely won’t introduce breaking changes, but here is a list of the dependency changes.

  • upgraded to Lit 3.0
  • upgraded to TypeScript 5
    • removed ttypescript and typescript-transform-paths dependencies as TypeScript 5 makes them obsolete
  • added @ctrl/tinycolor dependency
    • removed color dependency

Date picker

The mo-date-picker has been completely rewritten from scratch. The dependency to duet-date-picker has been removed, and it is now a completely custom component. Internally it uses the new mo-calendar to render the date picker popup.

It should now cover all the features of the DateTimeRangeInput component from DSUI, while allowing for more customization and some additional features.

Notable features
  • now includes proper keyboard interaction
  • users can slot in arbitrary content to footer and sidebar slots
  • full support for localization, with presets for enUS, es, fi, pl, ptBR
  • range selection from one input with two calendars
  • manual input that is parsed using the customizable format attribute
  • built-in time input, with options to make it readonly and to give it a default value
  • disabling certain dates from user selection using the min, max, and isDateDisabled attributes
  • mobile adaptiveness using the mo-drawer
Footer and sidebar slots

Localization
Suomi English Polish Español Português
<h5>Footer and sidebar slots</h5>
<mo-date-picker id="footer-date" selection="range" label="Choose date range">
  <div class="footer-div" slot="footer">
    <mo-button id="cancel-btn" variant="secondary">Cancel</mo-button>
    <mo-button id="apply-btn">Apply</mo-button>
  </div>
  <div class="sidebar-div" slot="sidebar">
    <mo-radio-group class="rg" hide-fieldset orientation="vertical">
      <mo-radio-button value="86400">Day</mo-radio-button>
      <mo-radio-button value="604800">Week</mo-radio-button>
      <mo-radio-button value="2592000">Month</mo-radio-button>
      <mo-radio-button value="31536000">Year</mo-radio-button>
    </mo-radio-group>
  </div>
  <div class="footer-div" slot="mobile-footer">
    <mo-button id="cancel-btn-mobile" variant="secondary">Cancel</mo-button>
    <mo-button id="apply-btn-mobile">Apply</mo-button>
  </div>
  <div class="sidebar-div" slot="mobile-sidebar">
    <mo-radio-group class="rg-mobile" hide-fieldset>
      <mo-radio-button value="86400">Day</mo-radio-button>
      <mo-radio-button value="604800">Week</mo-radio-button>
      <mo-radio-button value="2592000">Month</mo-radio-button>
      <mo-radio-button value="31536000">Year</mo-radio-button>
    </mo-radio-group>
  </div>
</mo-date-picker>
<br />

<h5>Localization</h5>
<div style="display: flex; flex-direction: column; gap: 8px;">
  <mo-select style="align-self: end;" value="fi">
    <mo-option value="fi">Suomi</mo-option>
    <mo-option value="en-US">English</mo-option>
    <mo-option value="pl">Polish</mo-option>
    <mo-option value="es">Español</mo-option>
    <mo-option value="pt-BR">Português</mo-option>
  </mo-select>
  <mo-date-picker
    id="localized"
    label="Valitse päivä"
    locale="fi"
    lang="fi"
    format="do MMMM, yyyy"
    closeOnSelection
    selection="range"
  ></mo-date-picker>
</div>

<script>
  const select = document.querySelector('mo-select');
  const datePicker = document.querySelector('#localized');
  const labels = {
    fi: 'Valitse päivä',
    'en-US': 'Choose date',
    'pt-BR': 'Escolha a data',
    es: 'Elegir fecha',
    pl: 'Wybierz datę'
  };
  select.addEventListener('mo-change', e => {
    datePicker.locale = e.target.value;
    datePicker.lang = e.target.value.split('-')[0];
    datePicker.label = labels[e.target.value];
  });

  const picker = document.querySelector('#footer-date');
  const radioGrp = document.querySelector('.rg');
  const radioGrpMobile = document.querySelector('.rg-mobile');
  const cancelBtn = document.querySelector('#cancel-btn');
  const applyBtn = document.querySelector('#apply-btn');
  const cancelBtnMobile = document.querySelector('#cancel-btn');
  const applyBtnMobile = document.querySelector('#apply-btn');
  cancelBtn.addEventListener('click', () => {
    picker.reset();
    picker.hide();
  });
  applyBtn.addEventListener('click', () => {
    picker.hide();
  });
  cancelBtnMobile.addEventListener('click', () => {
    picker.reset();
    picker.hide();
  });
  applyBtnMobile.addEventListener('click', () => {
    picker.hide();
  });
  radioGrp.addEventListener('mo-change', e => {
    const value = e.target.value;
    const today = new Date();
    const rangeEndMs = new Date().getTime() + parseInt(value) * 1000;
    const rangeEndDate = new Date(rangeEndMs);
    picker.value = [today, rangeEndDate];
  });
  radioGrpMobile.addEventListener('mo-change', e => {
    const value = e.target.value;
    const today = new Date();
    const rangeEndMs = new Date().getTime() + parseInt(value) * 1000;
    const rangeEndDate = new Date(rangeEndMs);
    picker.value = [today, rangeEndDate];
  });
</script>

<style>
  .footer-div,
  .sidebar-div {
    display: flex;
    align-items: center;
    gap: var(--mo-spacing-small);
    width: 100%;
    justify-content: flex-end;
    box-sizing: border-box;
  }
  .sidebar-div {
    flex-direction: column;
    gap: 0;
  }
  mo-radio-button {
    margin: 0;
  }
  mo-date-picker::part(sidebar) {
    padding: 0;
  }
  mo-date-picker::part(mobile-sidebar) {
    padding: 0;
  }
  h5 {
    margin: 0;
    margin-bottom: 1rem;
  }
  @media screen and (max-width: 576px) {
    .footer-div,
    .sidebar-div {
      flex-direction: row;
      width: 100%;
    }
    .footer-div {
      justify-content: flex-end;
    }
    .sidebar-div {
      justify-content: flex-start;
    }
  }
</style>

The mo-carousel component has also been completely renewed, as it was too opinionated an inflexible. The carousel slide has been renamed to carousel item to align with the names of other components. It no longer has pre-configured slots for content, and it does not apply a gradient overlay on the content. Users must now fully dictate the content and its layout themselves, and gradient overlays must also be implemented separately.

  • multiple slides per view
  • aspect ratio to customize the size of the carousel’s default viewport
  • scroll hint (show a bit of the next slide)
  • proper keyboard interaction

The carousel’s robust API makes it possible to extend and customize. This example syncs the active slide with a set of thumbnails, effectively creating a gallery-style carousel.

A snowy winter day at a quarry. Sun shines on the mining machine in a quarry. A busy city viewed from above. A large quarry for mining salt. A conveyor belt moving rocks in Lehigh Hanson.
Thumbnail by 1 Thumbnail by 2 Thumbnail by 3 Thumbnail by 4 Thumbnail by 5
<mo-carousel class="carousel-thumbnails" navigation loop>
  <mo-carousel-item>
    <img alt="A snowy winter day at a quarry." src="/assets/examples/carousel/winter-quarry.webp" />
  </mo-carousel-item>
  <mo-carousel-item>
    <img alt="Sun shines on the mining machine in a quarry." src="/assets/examples/carousel/sun-quarry.webp" />
  </mo-carousel-item>
  <mo-carousel-item>
    <img alt="A busy city viewed from above." src="/assets/examples/carousel/busy-city.webp" />
  </mo-carousel-item>
  <mo-carousel-item>
    <img alt="A large quarry for mining salt." src="/assets/examples/carousel/salt-quarry.webp" />
  </mo-carousel-item>
  <mo-carousel-item>
    <img alt="A conveyor belt moving rocks in Lehigh Hanson." src="/assets/examples/carousel/lehigh-hanson.webp" />
  </mo-carousel-item>
</mo-carousel>

<div class="thumbnails">
  <div class="thumbnails__scroller">
    <img alt="Thumbnail by 1" class="thumbnails__image active" src="/assets/examples/carousel/winter-quarry.webp" />
    <img alt="Thumbnail by 2" class="thumbnails__image" src="/assets/examples/carousel/sun-quarry.webp" />
    <img alt="Thumbnail by 3" class="thumbnails__image" src="/assets/examples/carousel/busy-city.webp" />
    <img alt="Thumbnail by 4" class="thumbnails__image" src="/assets/examples/carousel/salt-quarry.webp" />
    <img alt="Thumbnail by 5" class="thumbnails__image" src="/assets/examples/carousel/lehigh-hanson.webp" />
  </div>
</div>

<style>
  .carousel-thumbnails {
    --slide-aspect-ratio: 3 / 2;
  }

  .thumbnails {
    display: flex;
    justify-content: center;
  }

  .thumbnails__scroller {
    display: flex;
    gap: var(--mo-spacing-small);
    overflow-x: auto;
    scrollbar-width: none;
    scroll-behavior: smooth;
    scroll-padding: var(--mo-spacing-small);
  }

  .thumbnails__scroller::-webkit-scrollbar {
    display: none;
  }

  .thumbnails__image {
    width: 64px;
    height: 64px;
    object-fit: cover;

    opacity: 0.3;
    will-change: opacity;
    transition: 250ms opacity;

    cursor: pointer;
  }

  .thumbnails__image.active {
    opacity: 1;
  }
</style>

<script>
  {
    const carousel = document.querySelector('.carousel-thumbnails');
    const scroller = document.querySelector('.thumbnails__scroller');
    const thumbnails = document.querySelectorAll('.thumbnails__image');

    scroller.addEventListener('click', e => {
      const target = e.target;

      if (target.matches('.thumbnails__image')) {
        const index = [...thumbnails].indexOf(target);
        carousel.goToSlide(index);
      }
    });

    carousel.addEventListener('mo-slide-change', e => {
      const slideIndex = e.detail.index;

      [...thumbnails].forEach((thumb, i) => {
        thumb.classList.toggle('active', i === slideIndex);
        if (i === slideIndex) {
          thumb.scrollIntoView({
            block: 'nearest'
          });
        }
      });
    });
  }
</script>
import { useRef } from 'react';
import MOCarousel from '@metsooutotec/modes-web-components/dist/react/carousel';
import MOCarouselItem from '@metsooutotec/modes-web-components/dist/react/carousel-item';
import MODivider from '@metsooutotec/modes-web-components/dist/react/divider';
import MORange from '@metsooutotec/modes-web-components/dist/react/range';

const css = `
  .carousel-thumbnails {
    --slide-aspect-ratio: 3 / 2;
  }

  .thumbnails {
    display: flex;
    justify-content: center;
  }

  .thumbnails__scroller {
    display: flex;
    gap: var(--mo-spacing-small);
    overflow-x: auto;
    scrollbar-width: none;
    scroll-behavior: smooth;
    scroll-padding: var(--mo-spacing-small);
  }

  .thumbnails__scroller::-webkit-scrollbar {
    display: none;
  }

  .thumbnails__image {
    width: 64px;
    height: 64px;
    object-fit: cover;

    opacity: 0.3;
    will-change: opacity;
    transition: 250ms opacity;

    cursor: pointer;
  }

  .thumbnails__image.active {
    opacity: 1;
  }
`;

const images = [
  {
    src: '/assets/examples/carousel/winter-quarry.webp',
    alt: 'The sun shines on the mountains and trees (by Adam Kool on Unsplash'
  },
  {
    src: '/assets/examples/carousel/sun-quarry.webp',
    alt: 'A waterfall in the middle of a forest (by Thomas Kelly on Unsplash'
  },
  {
    src: '/assets/examples/carousel/busy-city.webp',
    alt: 'The sun is setting over a lavender field (by Leonard Cotte on Unsplash'
  },
  {
    src: '/assets/examples/carousel/salt-quarry.webp',
    alt: 'A field of grass with the sun setting in the background (by Sapan Patel on Unsplash'
  },
  {
    src: '/assets/examples/carousel/lehigh-hanson.webp',
    alt: 'A scenic view of a mountain with clouds rolling in (by V2osk on Unsplash'
  }
];

const App = () => {
  const carouselRef = useRef();
  const thumbnailsRef = useRef();
  const [currentSlide, setCurrentSlide] = useState(0);

  useEffect(() => {
    const thumbnails = Array.from(thumbnailsRef.current.querySelectorAll('.thumbnails__image'));

    thumbnails[currentSlide]..scrollIntoView({
      block: 'nearest'
    });
  }, [currentSlide]);

  const handleThumbnailClick = (index) => {
    carouselRef.current.goToSlide(index);
  }

  const handleSlideChange = (event) => {
    const slideIndex = e.detail.index;
    setCurrentSlide(slideIndex);
  }

  return (
    <>
      <MOCarousel className="carousel-thumbnails" navigation loop onMOSlideChange={handleSlideChange}>
        {images.map({ src, alt }) => (
          <MOCarouselItem>
            <img
              alt={alt}
              src={src}
            />
          </MOCarouselItem>
        )}
      </MOCarousel>

      <div class="thumbnails">
        <div class="thumbnails__scroller">
          {images.map({ src, alt }, i) => (
            <img
              alt={`Thumbnail by ${i + 1}`}
              className={`thumbnails__image ${i === currentSlide ? 'active' : ''}`}
              onClick={() => handleThumbnailClick(i)}
              src={src}
            />
          )}
        </div>
      </div>
      <style>{css}</style>
    </>
  );
};

Card

  • changed panel border color to mo-color-secondary-70
  • mo-card no longer has elevation (box-shadow) by default
    • manually apply the box-shadow (as shown below) if you wish to maintain the old style
New (3.0)
This card has a footer. You can put all sorts of things in it!
Preview
Old (< 3.0)
This card has a footer. You can put all sorts of things in it!
Preview
<mo-card class="card-footer">
  <div class="card-content">
    <mo-badge variant="success" size="medium">New (3.0)</mo-badge>
    <br />
    This card has a footer. You can put all sorts of things in it!
  </div>

  <div slot="footer">
    <mo-rating></mo-rating>
    <mo-button slot="footer" variant="primary">Preview</mo-button>
  </div>
</mo-card>

<mo-divider></mo-divider>

<mo-card class="card-footer old">
  <div class="card-content">
    <mo-badge size="medium">Old (< 3.0)</mo-badge>
    <br />
    This card has a footer. You can put all sorts of things in it!
  </div>

  <div slot="footer">
    <mo-rating></mo-rating>
    <mo-button slot="footer" variant="primary">Preview</mo-button>
  </div>
</mo-card>

<style>
  .card-footer {
    max-width: 300px;
  }

  .card-content {
    display: flex;
    flex-direction: column;
    gap: var(--mo-spacing-x-small);
  }

  .card-content mo-badge {
    align-self: flex-end;
  }

  .card-footer [slot='footer'] {
    display: flex;
    justify-content: space-between;
    align-items: center;
  }
  .old {
    box-shadow: var(--mo-elevation-1);
    --border-color: var(--mo-color-secondary-90);
  }
</style>

Input changes

This version includes a lot of small changes in various input components to bring them all in line in terms of styling, sizing, and naming.

Select

  • the new mo-option is now the child option element instead of mo-menu-item
  • the mo-select event has been renamed to mo-input, for consistency with other input component events
  • mo-select part control changed to combobox
  • renamed attribute max-tags-visible to max-options-visible

The mo-select no longer uses mo-menu-item as its internal option element. Replacing any references to mo-menu-item inside a select with mo-option should be enough for most users to migrate:

v2.x

<mo-select label="Select one">
  <mo-menu-item value="option-1">Option 1</mo-menu-item>
  <mo-menu-item value="option-2">Option 2</mo-menu-item>
  <mo-menu-item value="option-3">Option 3</mo-menu-item>
</mo-select>

v3.0

<mo-select label="Select one">
  <mo-option value="option-1">Option 1</mo-option>
  <mo-option value="option-2">Option 2</mo-option>
  <mo-option value="option-3">Option 3</mo-option>
</mo-select>
Option 1 Option 2 Option 3
<mo-select label="Select one">
  <mo-option value="option-1">Option 1</mo-option>
  <mo-option value="option-2">Option 2</mo-option>
  <mo-option value="option-3">Option 3</mo-option>
</mo-select>

Password toggling

  • renamed toggle-password attribute to password-toggle in mo-input to match the new password-visible attribute that allows for programmatic password visibility setting

Show password Hide password
<mo-input id="password-input" type="password" password-toggle></mo-input>
<br />
<mo-button id="visibility-toggle-v">Show password</mo-button>
<mo-button id="visibility-toggle-h">Hide password</mo-button>

<script>
  const btnV = document.getElementById('visibility-toggle-v');
  const btnH = document.getElementById('visibility-toggle-h');
  const input = document.getElementById('password-input');
  btnV.addEventListener('click', () => {
    input.passwordVisible = true;
  });
  btnH.addEventListener('click', () => {
    input.passwordVisible = false;
  });
</script>

Error and success state

The mo-input, mo-select, mo-combobox, mo-date-picker, and mo-time-picker now include the error, errorText, success, successText attributes that can be used to display error and success states easily. This is mainly useful for projects that use a third party validation library such as Zod to handle input validation. See the input example for an interactive example.


<mo-input label="Success" successText="Good input."></mo-input> <br />
<mo-input label="Error" errorText="Bad input."></mo-input>

Sizing

New

  • added new x-large size to mo-button
  • mo-checkbox now has a size attribute
  • default sizing is now consistent at 32px across multiple components such as mo-input, mo-select, mo-date-picker
Old default size (was medium = 40px) Option 1
New default size (is medium = 32px) Option 1
<div style="display: flex; flex-direction: column; gap: 16px;">
  Old default size (was medium = 40px)
  <mo-input placeholder="Input here" size="large"></mo-input>
  <mo-select size="large" value="1">
    <mo-option value="1">Option 1</mo-option>
  </mo-select>
</div>
<mo-divider></mo-divider>
<div style="display: flex; flex-direction: column; gap: 16px;">
  New default size (is medium = 32px)
  <mo-input placeholder="Input here"></mo-input>

  <mo-select value="1">
    <mo-option value="1">Option 1</mo-option>
  </mo-select>
</div>

All input components now follow the same sizing logic, so using them together in a form should look more uniform. The size attributes have been realigned so that e.g., the small size for different components always maps to the same pixel value. Below is a table of the new sizing tokens.

Name Token Value Small --mo-input-height-small 24px (1.5rem) Medium (default) --mo-input-height-medium 32px (2rem) Large --mo-input-height-large 40px (2.5rem) X-Large --mo-input-height-x-large 48px (3rem) XX-Large --mo-input-height-2x-large 64px (4rem)

Color picker

  • mo-color-picker no longer supports css variables as the value
  • removed default swatches from mo-color-picker
  • style is now rectangular instead of circular
<mo-color-picker value="#eb2814"></mo-color-picker>

New components

This version adds some low-level utility components such as the mo-label and mo-popup. The new mo-radio-button replaces the mo-toggle-button, and the new mo-option replaces the mo-menu-item as the element to include as children of the mo-select.

Calendar

The mo-calendar component shows a monthly view of the Gregorian calendar, optionally allowing users to interact with dates. Used internally in the new mo-date-picker.

<mo-calendar></mo-calendar>

Label

The mo-label is a simple, atomic component that is used internally within input components. It can be used to add a label for your custom input component that will be consistent with input component styling, and will have functionality such as the required attribute.

Custom label
<mo-label required>Custom label</mo-label>

Option

The mo-option replaces the mo-menu-item as the element to include as children of the mo-select. Options define the selectable items within various form controls such as select.

Option 1 Option 2 Option 3
<mo-select label="Select one">
  <mo-option value="option-1">Option 1</mo-option>
  <mo-option value="option-2">Option 2</mo-option>
  <mo-option value="option-3">Option 3</mo-option>
</mo-select>

Radio Button

The mo-radio-button is designed to be used with radio groups. When a radio button has focus, the arrow keys can be used to change the selected option just like standard radio controls. The mo-radio-button replaces the mo-toggle-button component, which has now been marked as deprecated.

Option 1 Option 2 Option 3
<mo-radio-group label="Select an option" name="a" value="1">
  <mo-radio-button value="1">Option 1</mo-radio-button>
  <mo-radio-button value="2">Option 2</mo-radio-button>
  <mo-radio-button value="3">Option 3</mo-radio-button>
</mo-radio-group>

mo-popup is a low-level utility built specifically for positioning elements. Do not mistake it for a tooltip or similar because it does not facilitate an accessible experience! Almost every correct usage of <mo-popup> will involve building other components. It should rarely, if ever, occur directly in your HTML.

You can use it as a building block for your own custom components where you need to create a popup that is relative to a parent element. The popup is used internally in components such as mo-dropdown and mo-select.

<div class="popup-overview">
  <mo-popup placement="top" active>
    <span slot="anchor"></span>
    <div class="box"></div>
  </mo-popup>

  <div class="popup-overview-options">
    <mo-select label="Placement" name="placement" value="top" class="popup-overview-select">
      <mo-option value="top">top</mo-option>
      <mo-option value="top-start">top-start</mo-option>
      <mo-option value="top-end">top-end</mo-option>
      <mo-option value="bottom">bottom</mo-option>
      <mo-option value="bottom-start">bottom-start</mo-option>
      <mo-option value="bottom-end">bottom-end</mo-option>
      <mo-option value="right">right</mo-option>
      <mo-option value="right-start">right-start</mo-option>
      <mo-option value="right-end">right-end</mo-option>
      <mo-option value="left">left</mo-option>
      <mo-option value="left-start">left-start</mo-option>
      <mo-option value="left-end">left-end</mo-option>
    </mo-select>
    <mo-input type="number" name="distance" label="distance" value="0"></mo-input>
    <mo-input type="number" name="skidding" label="Skidding" value="0"></mo-input>
  </div>

  <div class="popup-overview-options">
    <mo-switch name="active" checked>Active</mo-switch>
    <mo-switch name="arrow">Arrow</mo-switch>
  </div>
</div>

<script>
  const container = document.querySelector('.popup-overview');
  const popup = container.querySelector('mo-popup');
  const select = container.querySelector('mo-select[name="placement"]');
  const distance = container.querySelector('mo-input[name="distance"]');
  const skidding = container.querySelector('mo-input[name="skidding"]');
  const active = container.querySelector('mo-switch[name="active"]');
  const arrow = container.querySelector('mo-switch[name="arrow"]');

  select.addEventListener('mo-change', () => (popup.placement = select.value));
  distance.addEventListener('mo-input', () => (popup.distance = distance.value));
  skidding.addEventListener('mo-input', () => (popup.skidding = skidding.value));
  active.addEventListener('mo-change', () => (popup.active = active.checked));
  arrow.addEventListener('mo-change', () => (popup.arrow = arrow.checked));
</script>

<style>
  .popup-overview mo-popup {
    --arrow-color: var(--mo-color-primary-7);
  }

  .popup-overview span[slot='anchor'] {
    display: inline-block;
    width: 150px;
    height: 150px;
    border: dashed 2px var(--mo-color-secondary-70);
    margin: 50px;
  }

  .popup-overview .box {
    width: 100px;
    height: 50px;
    background: var(--mo-color-primary-7);
    border-radius: var(--mo-border-radius-medium);
  }

  .popup-overview-options {
    display: flex;
    flex-wrap: wrap;
    align-items: end;
    gap: 1rem;
  }

  .popup-overview-options mo-select {
    width: 160px;
  }

  .popup-overview-options mo-input {
    width: 100px;
  }

  .popup-overview-options + .popup-overview-options {
    margin-top: 1rem;
  }
</style>

Other minor changes

Theming

  • the info color has been updated to be more aligned with the brand
  • added new gray theme, try it out using the theme selector on the top right, see the theming documentation for more information
  • added new neutral color palette to the light.css and dark.css theme files
  • added new accessibility documentation, with a tool to check accessibility between two Modes colors

Tab

  • tabs are no longer all UPPERCASE, to align with branding writing guidelines
  • keyboard interaction fixed
  • styling updated to be consistent across different tab group positions, non-active tabs are no longer bold in text
General Custom Advanced Disabled This is the general tab panel. This is the custom tab panel. This is the advanced tab panel. This is a disabled tab panel.
<mo-tab-group>
  <mo-tab slot="nav" panel="general">General</mo-tab>
  <mo-tab slot="nav" panel="custom">Custom</mo-tab>
  <mo-tab slot="nav" panel="advanced">Advanced</mo-tab>
  <mo-tab slot="nav" panel="disabled" disabled>Disabled</mo-tab>

  <mo-tab-panel name="general">This is the general tab panel.</mo-tab-panel>
  <mo-tab-panel name="custom">This is the custom tab panel.</mo-tab-panel>
  <mo-tab-panel name="advanced">This is the advanced tab panel.</mo-tab-panel>
  <mo-tab-panel name="disabled">This is a disabled tab panel.</mo-tab-panel>
</mo-tab-group>

Progress

  • mo-progress-ring and mo-progress-bar track color changed from secondary-70 to secondary-90, added outline around mo-progress-bar.
  • mo-progress-bar has new appearance attribute, which can be used to render success or alert progress bar to visually show the status of the progress.
  • changed the default track width of progress ring to 8px
  • mo-progress-ring indicator now has a squared tip to align with branding
<div style="display: flex; flex-direction: column; gap: 16px;">
  <mo-progress-ring indeterminate value="40"></mo-progress-ring>
  <mo-progress-bar value="10"></mo-progress-bar>
  <mo-progress-bar value="33" appearance="success"></mo-progress-bar>
  <mo-progress-bar value="66" appearance="alert"></mo-progress-bar>
</div>

Radio

  • refactored mo-radio, mo-radio-group
  • mo-radio-group now has an orientation attribute to have vertical layouts for the mo-radio-buttons
  • mo-radio-groups now have fieldset styling by default (subtle border + padding), but it can be hidden using hide-fieldset
Option 1 Option 2 Option 3
Option 1 Option 2 Option 3
Option 1 Option 2 Option 3
<mo-radio-group label="Select an option" name="a" value="1">
  <mo-radio value="1">Option 1</mo-radio>
  <mo-radio value="2">Option 2</mo-radio>
  <mo-radio value="3">Option 3</mo-radio>
</mo-radio-group>

<br />

<mo-radio-group hide-fieldset name="a" value="1">
  <mo-radio-button value="1">Option 1</mo-radio-button>
  <mo-radio-button value="2">Option 2</mo-radio-button>
  <mo-radio-button value="3">Option 3</mo-radio-button>
</mo-radio-group>

<br />

<mo-radio-group hide-fieldset orientation="vertical" name="a" value="1">
  <mo-radio-button value="1">Option 1</mo-radio-button>
  <mo-radio-button value="2">Option 2</mo-radio-button>
  <mo-radio-button value="3">Option 3</mo-radio-button>
</mo-radio-group>

Time picker

  • now has an options attribute that can be used to customize the time suggestions
  • now shows the invalid state inside the input, rather than beside the label
  • has a new no-suggestions attribute to disable generation of suggestions
  • now has properly translated default error messages
<mo-time-picker
  help-text="Find available times in the dropdown, or input a manual time to submit a request for that time slot."
  min="12:00"
  max="18:00"
  id="custom"
  label="Appointment time"
></mo-time-picker>
<script>
  const picker = document.getElementById('custom');
  picker.options = [
    { text: '15:35', value: '15:35' },
    { text: '16:20', value: '16:20' },
    { text: '16:45', value: '16:45' },
    { text: '17:30', value: '17:30' }
  ];
</script>

Rating

  • mo-rating now has proper keyboard controls, improved mouse detection logic
  • introduced new mo-hover event
  • added label attribute to announce the rating to assistive devices for improved accessibility
  • fixed icon style, changed unselected color
<mo-rating id="demo-rating" value="3" label="Demo rating."></mo-rating>

<script>
  const rating = document.querySelector('#demo-rating');
  rating.addEventListener('mo-hover', e => {
    console.log(e.detail);
  });
</script>

Chip

  • now has a new outlined variant
Info Success Warning Alert
<mo-chip pill variant="info" outline>
  <mo-icon name="info" slot="suffix"></mo-icon>
  Info
</mo-chip>
<mo-chip pill variant="success" outline>
  <mo-icon name="ok-circle" slot="suffix"></mo-icon>
  Success
</mo-chip>
<mo-chip pill variant="warning" outline>
  <mo-icon name="exclamation-mark-circle" slot="suffix"></mo-icon>
  Warning
</mo-chip>
<mo-chip pill variant="alert" outline>
  <mo-icon name="alarm" slot="suffix"></mo-icon>
  Alert
</mo-chip>

Internal changes

  • refactored all alias imports (~/*) to use relative imports (e.g. ../..) from component files (.ts)
  • removed all style imports from .css files, moved generic component styles to the main component file (.ts) for performance reasons
  • moved internal dependencies to static dependencies variable for each component for clarity
  • refactored every component to use relative imports instead of the import alias as it was causing issues with the test plugin

v2.0 (May 17, 2023)

Table

The mo-table component has been completely rewritten. It now comes with mo-table-row, mo-table-cell, mo-table-head, mo-table-body and mo-table-head-cell components that can be used to compose a properly styled table, that comes with additional functionality such as row selection, sorting and virtualization. Check out the new documentation for usage and code examples.

The mo-carousel and mo-carousel-slide components have been refactored to be more composable, instead of relying on attributes/properties for customization. This means they are more flexible, customizable, and consistent with other Modes UI components.

The pagination dots inside the mo-carousel are no longer enabled by default. Use the pagination attribute to enable them.

Previously the action button inside the carousel was defined using properties linkLabel, linkUrl and linkTarget. Now the button should be slotted in to the button slot on the mo-carousel-slide. The attribute imageUrl has been removed, and the background image should now be supplied in the background slot of the mo-carousel-slide.

The attribute introText has been renamed to subtitle.

v1.x

<mo-carousel-slide
  introText="extra text"
  linkLabel="Button"
  linkUrl="https://metso.com/"
  linkTarget="_blank"
  imageUrl="https://images.pexels.com/photos/257700/pexels-photo-257700.jpeg"
></mo-carousel-slide>

v2.0

<mo-carousel-slide subtitle="extra text">
  <img
    slot="background"
    alt="industrial plant"
    src="https://images.pexels.com/photos/257700/pexels-photo-257700.jpeg"
  />
  <mo-button href="https://metso.com" target="_blank" slot="button">Button</mo-button>
</mo-carousel-slide>

Switch

The mo-switch component no longer has the darkMode attribute. Instead, creating a dark mode variant of the mo-switch should be accomplished using the prefix and suffix slots. Alongside this change, the checked state no longer has the ok checkmark icon by default, and it must be added using the suffix slot as shown in the documentation.

v1.x

<mo-switch> Switch </mo-switch>

<mo-switch darkMode> Dark mode toggle </mo-switch>

v2.0

<mo-switch>
  <mo-icon name="ok" slot="suffix"></mo-icon>
  Switch
</mo-switch>

<mo-switch>
  <mo-icon name="sun" slot="prefix"></mo-icon>
  <mo-icon name="moon-fill" slot="suffix"></mo-icon>
  Dark mode toggle
</mo-switch>

The mo-header component is no longer contained by default. This behaviour must be enabled using the contained attribute on the mo-header. All of the code examples on this site include this attribute to ensure the drawer opens within the example, not the context of the entire page.