UXP: Updating one <select> dynamically also affects other dropdowns

Environment

  • Photoshop: 27.3.1

  • UXP manifestVersion 5

  • OS: Microsoft Windows 11 Pro


Summary

When dynamically replacing the options of one <select> element, other <select> elements appear to be affected as well.
The issue becomes visible after a short delay or when hovering over the other dropdowns.


Expected Behavior

Only the targeted <select> element should update.

Other dropdowns should remain unchanged.


Actual Behavior

After clicking the button to replace the top dropdown’s options:

  • Other dropdowns initially appear unchanged.

  • After waiting briefly or hovering over them, their options visually update.

  • This suggests a rendering or invalidation issue.

If this behavior is a known limitation or rendering issue, any recommended workaround would be greatly appreciated.

The following is a sample code.
Since I was unable to attach files, I am pasting it directly below.

(() => {
  const targetSelect = document.getElementById("dropdown-1");
  const replaceButton = document.getElementById("replace-top-dropdown");
  const replaceSecondButton = document.getElementById("replace-second-dropdown");
  // const randomizeButton = document.getElementById("randomize-top-selection");
  const statusText = document.getElementById("status");
  const internalTemplate = document.getElementById("internal-html-option-template");

  if (!targetSelect || !replaceButton || !replaceSecondButton || !statusText || !internalTemplate) {
    console.error("[dynamicdropdown] required element was not found", {
      targetSelect,
      replaceButton,
      replaceSecondButton,
      // randomizeButton,
      statusText,
      internalTemplate,
    });
    return;
  }

  const dynamicOptions = [
    { value: "dynamic-metal", label: "Metal" },
    { value: "dynamic-wood", label: "Wood" },
    { value: "dynamic-stone", label: "Stone" },
    { value: "dynamic-fabric", label: "Fabric" },
  ];

  const dynamicOptionsSecond = [
    { value: "logic2-carbon", label: "Carbon" },
    { value: "logic2-rubber", label: "Rubber" },
    { value: "logic2-ceramic", label: "Ceramic" },
    { value: "logic2-glass", label: "Glass" },
  ];

  function updateStatus(message) {
    statusText.textContent = message;
  }

  function getDropdownSnapshot() {
    const selects = Array.from(document.querySelectorAll("select"));
    return selects.map((select) => ({
      id: select.id,
      selectedIndex: select.selectedIndex,
      selectedValue: select.value,
      options: Array.from(select.options).map((option) => ({
        value: option.value,
        text: option.text,
      })),
    }));
  }

  function removeInternalHtmlTemplate() {
    const parent = internalTemplate.parentElement;
    if (!parent) {
      console.warn("[dynamicdropdown] templateの親要素がありません。", { internalTemplate });
      return;
    }
    parent.removeChild(internalTemplate);
    console.log("[dynamicdropdown] 内部HTML(template)を削除しました");
  }

  function buildOptions(optionDefs) {
    return optionDefs.map(({ value, label }) => {
      const option = document.createElement("option");
      option.value = value;
      option.textContent = label;
      return option;
    });
  }

  function appendDynamicOptions() {
    targetSelect.innerHTML = "";

    dynamicOptions.forEach((optionDef) => {
      const option = document.createElement("option");
      option.value = optionDef.value;
      option.textContent = optionDef.label;
      targetSelect.appendChild(option);
    });

    targetSelect.selectedIndex = 0;
    console.log("[dynamicdropdown] 動的オプションを追加しました", {
      optionCount: targetSelect.options.length,
      firstOption: targetSelect.options[0]?.value,
    });
  }

  function replaceTopOptionsWithFragment() {
    const options = buildOptions(dynamicOptionsSecond);
    const fragment = document.createDocumentFragment();
    options.forEach((option) => {
      fragment.appendChild(option);
    });
    targetSelect.replaceChildren(fragment);
    targetSelect.selectedIndex = 0;
    console.log("[dynamicdropdown] トップドロップダウンへ動的オプションを追加しました(logic=replaceChildren-fragment)", {
      optionCount: targetSelect.options.length,
      firstOption: targetSelect.options[0]?.value,
    });
  }

  replaceButton.addEventListener("click", () => {
    const beforeSnapshot = getDropdownSnapshot();
    console.log("[dynamicdropdown] 置換ボタン(トップ/ロジック1)が押されました");

    removeInternalHtmlTemplate();
    appendDynamicOptions();

    console.log("[dynamicdropdown] ドロップダウン状態(ロジック1置換前/後)", {
      before: beforeSnapshot,
      after: getDropdownSnapshot(),
    });
    updateStatus("Dropdown 1 was switched to dynamic options using logic 1 (innerHTML clear + append)");
  });

  replaceSecondButton.addEventListener("click", () => {
    const beforeSnapshot = getDropdownSnapshot();
    console.log("[dynamicdropdown] 置換ボタン(トップ/ロジック2)が押されました");

    replaceTopOptionsWithFragment();

    console.log("[dynamicdropdown] ドロップダウン状態(ロジック2置換前/後)", {
      before: beforeSnapshot,
      after: getDropdownSnapshot(),
    });
    updateStatus("Dropdown 1 was switched to dynamic options using logic 2 (replaceChildren + fragment)");
  });

  // randomizeButton.addEventListener("click", () => {
  //   const beforeSnapshot = getDropdownSnapshot();
  //   const optionCount = targetSelect.options.length;
  //   if (optionCount <= 0) {
  //     console.warn("[dynamicdropdown] randomize skipped: no options", {
  //       optionCount,
  //     });
  //     updateStatus("Randomize is unavailable because Dropdown 1 has no options");
  //     return;
  //   }
  //   const randomIndex = Math.floor(Math.random() * optionCount);
  //   const beforeValue = targetSelect.value;
  //   targetSelect.selectedIndex = randomIndex;
  //   const afterValue = targetSelect.value;
  //   console.log("[dynamicdropdown] Dropdown 1 selection was randomized", {
  //     beforeValue,
  //     afterValue,
  //     randomIndex,
  //     optionCount,
  //     beforeSnapshot,
  //     afterSnapshot: getDropdownSnapshot(),
  //   });
  //   updateStatus(`Dropdown 1 selection was randomized: ${afterValue}`);
  // });
})();
<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <link rel="stylesheet" href="styles.css" />
    <title>Dynamic Dropdown Sample</title>
  </head>
  <body>
    <main class="container">
      <h1>Dynamic Dropdown Sample</h1>
      <p class="hint">
        Initially, four dropdowns are rendered statically. Use the buttons to dynamically rebuild only the first dropdown.
      </p>

      <section class="control-group">
        <label for="dropdown-1">Dropdown 1 (dynamic target)</label>
        <select id="dropdown-1" data-role="replace-target">
          <option value="initial-a">Static A</option>
          <option value="initial-b">Static B</option>
          <option value="initial-c">Static C</option>
        </select>
      </section>

      <section class="control-group">
        <label for="dropdown-2">Dropdown 2</label>
        <select id="dropdown-2">


          <option value="shape-circle">Circle</option>
          <option value="shape-square">Square</option>
          <option value="shape-triangle">Triangle</option>
        </select>
      </section>

      <section class="control-group">
        <label for="dropdown-3">Dropdown 3</label>
        <select id="dropdown-3">
          <option value="size-small">Small</option>
          <option value="size-medium">Medium</option>
          <option value="size-large">Large</option>
        </select>
      </section>

      <section class="control-group">
        <label for="dropdown-4">Dropdown 4</label>
        <select id="dropdown-4">
          <option value="blend-normal">Normal</option>
          <option value="blend-multiply">Multiply</option>
          <option value="blend-screen">Screen</option>
        </select>
      </section>

      <button id="replace-top-dropdown" type="button">Dynamically rewrite Dropdown 1 options (innerHTML rewrite)</button>
      <button id="replace-second-dropdown" type="button">Dynamically rewrite Dropdown 1 options (replace)</button>
      <!-- <button id="randomize-top-selection" type="button">Randomize Dropdown 1 selection</button> -->

      <p id="status" class="status">Ready</p>

      <template id="internal-html-option-template">
        <option value="dynamic-metal">Metal</option>
        <option value="dynamic-wood">Wood</option>
        <option value="dynamic-stone">Stone</option>
        <option value="dynamic-fabric">Fabric</option>
      </template>
    </main>
    <script src="main.js"></script>
  </body>
</html>
{
  "id": "com.example.dynamicdropdownsample",
  "name": "Dynamic Dropdown Sample",
  "version": "0.1.0",
  "main": "index.html",
  "host": {
    "app": "PS",
    "minVersion": "24.0.0"
  },
  "manifestVersion": 5,
  "entrypoints": [
    {
      "type": "panel",
      "id": "dynamic-dropdown-panel",
      "label": "Dynamic Dropdown Sample",
      "minimumSize": {
        "width": 320,
        "height": 320
      },
      "maximumSize": {
        "width": 640,
        "height": 800
      },
      "preferredDockedSize": {
        "width": 360,
        "height": 420
      },
      "preferredFloatingSize": {
        "width": 420,
        "height": 500
      }
    }
  ]
}

thanks.

1 Like