Implement an automatic annotation and field tab ordering

In document annotation workflows, maintaining a logical tab order for fields is essential for smooth user navigation and accessibility. Nutrient’s setPageTabOrder API offers a reliable way to set a customized tab ordering for annotations, enabling users to cycle through annotations in a meaningful sequence. This guide walks you through implementing an automatic tab order based on field positions using a JavaScript-based approach. We’ll address the challenges introduced in Nutrient versions after 2024.3.0 and provide a solution that works across affected versions.

Why tab ordering matters

Annotations are often used in PDF forms, where they represent interactive fields, such as text boxes and checkboxes. By default, annotations may not follow a logical visual order when tabbed through, which can cause frustration and reduce accessibility. Our solution programmatically defines the tab sequence based on field positions, creating a more intuitive navigation experience.

Setting up tab order initialization

We begin by initializing the tab order setup on document load using initializeTabOrder. This function sets a tab order for each page by sorting annotations based on their vertical and horizontal positions, so users can move sequentially from top-left to bottom-right.

Code overview

Here’s the code for setting up and maintaining the tab order on each page:

let isTabOrderInitialized = false;

const initializeTabOrder = async (instance) => {
  // Return early if already initialized.
  if (!isTabOrderInitialized) {
    try {
      // Initialize tab reordering for each page.
      await Promise.all(
        Array.from({
          length: instance.totalPageCount
        }).map((_, pageIndex) =>
          instance.setPageTabOrder(
            pageIndex,
            (currentTabOrderedAnnotations) =>
              currentTabOrderedAnnotations
                .sort((a, b) => a.boundingBox.top - b.boundingBox.top)
                .map((annotation) => annotation.id)
          )
        )
      );
      isTabOrderInitialized = true;
      console.log("Tab order initialized");
    } catch (error) {
      console.error("Failed to initialize tab order:", error);
      throw error;
    }
  }
};

This function initializes the tab order across all pages on document load. Each page’s annotations are sorted by their boundingBox.top value, with ties broken by boundingBox.left. The sorted order is then mapped to annotation IDs to set the order using instance.setPageTabOrder.

Handling dynamic annotation creation and updates

Annotations may be added or updated after initialization, requiring dynamic updates to the tab order. For this, we use annotations.create and annotations.update event listeners to reorder annotations whenever a new annotation is created or an existing one is modified:

const annotationReorder = async (annotations) => {
  const annotation = annotations?.get(0);

  if (annotation) {
    const currentTabOrder = await globalInstance.getPageTabOrder(
      annotation.pageIndex
    );

    // Add annotation to tab order if it's missing.
    if (!currentTabOrder?.includes(annotation.id)) {
      currentTabOrder.push(annotation.id);
    }

    await globalInstance.setPageTabOrder(
      annotation.pageIndex,
      (currentTabOrderedAnnotations) =>
        currentTabOrderedAnnotations
          .sort((a, b) =>
            a.boundingBox.top !== b.boundingBox.top
              ? a.boundingBox.top - b.boundingBox.top
              : a.boundingBox.left - b.boundingBox.left
          )
          .map((annotation) => annotation.id)
    );
  }
};

This function first checks if the annotation exists and isn’t already part of the tab order. If it’s missing, it’s added to the list, and then the tab order is updated based on the position-based sorting logic described above.

Addressing the known bug in Nutrient 2024.3.1 to 2024.7.0

A bug affects setPageTabOrder in versions 2024.3.1 to 2024.7.0, where the tab ordering fails to update correctly in certain cases. To work around this, we use renderPageCallback to ensure initializeTabOrder is called whenever a page is rendered, maintaining correct tab behavior.

Here’s the code that integrates our solution with the Nutrient document load process, including this workaround:

let globalInstance = null;

PSPDFKit.load({
  ...baseOptions,
  theme: PSPDFKit.Theme.DARK,
  initialViewState: new PSPDFKit.ViewState({
    prerenderedPageSpreads: null
  }),
  renderPageCallback: function (ctx, pageIndex, pageSize) {
    initializeTabOrder(globalInstance);
  }
}).then(async (instance) => {
  globalInstance = instance;

  // Event listeners for dynamic annotation reordering
  instance.addEventListener("annotations.create", annotationReorder);
  instance.addEventListener("annotations.update", annotationReorder);
});

In this setup:

  • initializeTabOrder is invoked within renderPageCallback, ensuring the tab order is reset for any page that renders.

  • Event listeners for annotations.create and annotations.update maintain the tab order dynamically as annotations are added or modified.

This approach works reliably across versions, including those affected by the bug, making it a versatile solution for maintaining tab order consistency.

Conclusion

By implementing this solution, you ensure annotations in Nutrient-powered documents follow a logical tab order based on their positions on the page. This code is ideal for customers requiring Adobe-like tab ordering. Although a bug currently impacts the setPageTabOrder method in some versions, the workaround with renderPageCallback restores expected functionality. This approach provides a seamless user experience, with enhanced accessibility and ease of navigation through annotations.

References

  • Nutrient Playground example — For a working Playground example, see the tab ordering Playground.

  • Nutrient Web API documentation — For more details on setPageTabOrder and related methods, refer to the Nutrient API reference.

  • Nutrient knowledge base— See more examples of setPageTabOrder use and discussion in our knowledge base.