Skip to content

[Bug]: FilterableMultiSelect uses the caller id (not its useId instance id) for menuId/inputId and getElementById-based outside-click, so two instances with the same id break the second one #23115

Description

@matthewpagacik12

Package

@carbon/react

Browser

Chrome

Package version

v1.112.0

React version

v18.3.1

Description

FilterableMultiSelect id handling issue

FilterableMultiSelect derives its internal DOM ids and its outside-click detection from the caller-supplied id prop instead of from a unique per-instance id.

As a result, when two FilterableMultiSelect instances are rendered with the same id, the second instance becomes unusable—opening its menu and clicking an item immediately closes the menu (focus is lost, no value can be selected).


Current use of useId

This is notable because FilterableMultiSelect creates a unique instance id with useId(), but only uses it for the helper text:

import { useId } from '../../internal/useId';

// ...
const filterableMultiSelectInstanceId = useId();

// ...
const helperId = !hasHelper
  ? undefined
  : `filterablemultiselect-helper-text-${filterableMultiSelectInstanceId}`;

However, the ids that actually need to be unique are derived from the caller id:

const labelId = `${id}-label`;
const menuId  = `${id}__menu`;
const inputId = `${id}-input`;

useCombobox({
  /* ... */
  id,
  labelId,
  menuId,
  inputId,
  /* ... */
});

Outside-click handler (root cause)

The functional break is caused by the outside-click handler, which looks the component up by id:

const handleClickOutside = (event: MouseEvent) => {
  const target = event.target;

  if (!(target instanceof HTMLElement)) {
    return;
  }

  const wrapper = document
    .getElementById(id) // ← returns the FIRST element with this id
    ?.closest(`.${prefix}--multi-select__wrapper`);

  if (wrapper && !wrapper.contains(target)) { // ← second instance's clicks look "outside"
    if (isOpen || inputFocused) {
      setIsOpen(false);        // ← force-closes the menu
      setInputFocused(false);
      setInputValue('');
    }
  }
};

When two instances share id, document.getElementById(id) always resolves to the first instance's ListBox.
So for the second instance:

  • wrapper is the first instance's wrapper.
  • A click on the second instance is not contained by the first instance's wrapper.
  • Therefore !wrapper.contains(target) is true, and the handler runs, closing the menu before the selection registers.

Accessibility defect

Additionally:

  • The duplicated menuId (${id}__menu) and
  • The input’s aria-controls={menuId}

mean both instances point their aria-controls at the same element id, which is an a11y defect independent of the click handler.


Comparison with ComboBox/Dropdown

ComboBox and Dropdown do not exhibit this issue:

  • They do not use getElementById(id)-based outside-click detection.
  • As a result, multiple instances with the same id remain independently operable.

Reproduction/example

https://jvmiqgrynwgithub-lf0a--5173--017acfb7.local-credentialless.webcontainer.io/

Steps to reproduce

  1. Render the two FilterableMultiSelects above (same id="field-1").
  2. Open the first one and select an item → works.
  3. Open the second one and click an item.

Suggested Severity

High (Severity 2) = User cannot complete task, and/or no workaround within the user experience of a given component.

Project name

N/A

Code of Conduct

Metadata

Metadata

Assignees

No one assigned

    Type

    Fields

    Severity

    High

    Projects

    Status
    🕵️‍♀️ Triage

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions