Background

mirador-annotations is a plugin that adds annotation functionality to the IIIF viewer Mirador.

The previous project configuration was as follows:

  • Build tool: nwb (Create React App based)
  • UI library: Material-UI v4
  • Mirador: 3.x
  • React: 17.x

However, the following issues had arisen:

  1. nwb maintenance discontinued - nwb had not been updated for a long time, and dependency conflicts occurred frequently
  2. npm install failures - Old dependencies made setup in new environments difficult
  3. Security vulnerabilities - Numerous vulnerability warnings from outdated packages

To resolve these issues, we decided to migrate to:

  • Build tool: Vite
  • UI library: MUI v7
  • Mirador: 4.x
  • React: 18.x

Migration Overview

1. Build Tool Migration (nwb to Vite)

We deleted the nwb configuration files and created a new vite.config.js.

Key points:

// vite.config.js
export default defineConfig(({ mode }) => {
  const env = loadEnv(mode, process.cwd(), '');

  return {
    // Because draft-js references global
    define: {
      global: 'globalThis',
    },

    // Resolve duplicate packages
    resolve: {
      dedupe: [
        '@emotion/react',
        '@emotion/styled',
        'react',
        'react-dom',
      ],
    },
  };
});

2. Material-UI Migration (v4 to v7)

  • Changed @material-ui/* to @mui/*
  • Replaced makeStyles with sx prop
  • Adapted to Grid component API changes (item and xs props consolidated into size)
// Before (MUI v4)
Grid item xs={12}>

// After (MUI v7)
Grid size={12}>

3. Adapting to Mirador 4.x

In Mirador 4.x, the import methods for actions and selectors changed:

// Before (Mirador 3.x)
import { actions, selectors } from 'mirador';
actions.receiveAnnotation(...)
selectors.getVisibleCanvases(...)

// After (Mirador 4.x)
import { receiveAnnotation, getVisibleCanvases } from 'mirador';
receiveAnnotation(...)
getVisibleCanvases(...)

Pitfalls Encountered

1. menuItemRef.current.focus is not a function Error

Symptom

When navigating to a canvas with annotations, the following error occurred:

TypeError: menuItemRef.current.focus is not a function

Cause

Mirador 4.x’s CanvasAnnotations component uses <MenuList autoFocusItem>. This autoFocusItem feature automatically focuses on the first item in the list, but requires the focused element to have a focus() method.

The problem was in the CanvasListItem component:

// Problematic code
class CanvasListItem extends Component {
  render() {
    return (
      div> {/* div receives ref, but has no focus() method */}
        {this.props.children}
      div>
    );
  }
}

MUI’s MenuItem uses refs internally for focus control, but since CanvasListItem was a class component wrapping a <div>, refs were not forwarded correctly, resulting in the focus() method not being found.

Solution

We rewrote CanvasListItem as a function component, using forwardRef to forward the ref directly to the <li> element:

import React, { useState, useContext, forwardRef } from 'react';

const CanvasListItem = forwardRef(({
  annotationid, children, ...otherProps
}, ref) => {
  const [isHovering, setIsHovering] = useState(false);
  // ...

  return (
    li
      ref={ref}  // Forward ref directly to li element
      onMouseEnter={handleMouseEnter}
      onMouseLeave={handleMouseLeave}
      {...otherProps}
    >
      {/* ... */}
      {children}
    li>
  );
});

Lessons Learned

  • MUI v5+’s MenuList’s autoFocusItem expects child components to properly forward refs
  • When creating custom list item components, forwardRef must be used to pass refs to DOM elements
  • When migrating from class components to function components, careful attention to ref handling is required

2. CompanionWindow Not Displaying

Symptom

Clicking the “Create new annotation” button displayed nothing. Checking the console revealed:

  • The addCompanionWindow action was dispatched normally
  • The AnnotationCreation component’s render() was being called
  • However, nothing appeared on screen

Additionally, the following warning was being issued:

Warning: Failed prop type: The prop `direction` is marked as required
in `CompanionWindow`, but its value is `undefined`.

Cause

Mirador exports two versions of the CompanionWindow component:

  1. CompanionWindow - Base component (props must be passed manually)
  2. ConnectedCompanionWindow - Redux-connected version (props are automatically retrieved from the store)

The pre-migration code was using CompanionWindow directly, but in Mirador 4.x, ConnectedCompanionWindow needed to be used instead.

// Problematic code
import { CompanionWindow } from 'mirador';

// CompanionWindow requires direction, position, etc. to be
// passed manually, but these are in the Redux store

Solution

Changed to import ConnectedCompanionWindow:

// Fixed code
import { ConnectedCompanionWindow as CompanionWindow } from 'mirador';

This allowed the required props such as direction and position to be automatically retrieved from the Redux store.

Lessons Learned

  • Mirador components may have both a base version and a Redux-connected version
  • When developing plugins, it is necessary to verify which version should be used
  • When “a component is being rendered but nothing is displayed,” suspect that required props may be missing

Other Issues Addressed

draft-js global Reference Error

draft-js references the global object, which does not exist in browser environments. Resolved through Vite configuration:

define: {
  global: 'globalThis',
}

@emotion/react Duplication Warning

Occurred because both Mirador and this plugin bundled @emotion/react. Resolved with resolve.dedupe:

resolve: {
  dedupe: ['@emotion/react', '@emotion/styled', 'react', 'react-dom'],
}

Continued Need for legacy-peer-deps

Because @psychobolt/react-paperjs requires React 17 as a peer dependency, legacy-peer-deps=true in .npmrc is still necessary.


Summary

The two issues that took the most time during this migration were:

  1. ref forwarding issue - Required understanding MUI’s internal behavior and handling refs correctly
  2. Using Connected components - Required understanding Mirador’s export structure

Both were resolved by understanding the internal implementations of the libraries. During migration work, it is important to examine not only error messages but also the library source code.

Demo

The post-migration behavior can be tested at the following demo site:

https://nakamura196.github.io/mirador-annotations/

References