Dropdown in embedded Pavlovia survey is showing twice

I have been testing out embedding Pavlovia surveys via the Pavlovia Survey builder component, and have run into an interesting issue.

I have created a survey with a dropdown menu question, and when running the survey alone via the “run survey” link (i.e., not embedded in the experiment), this displays as expected:

However, when running the experiment with the survey embedded, the dropdown element shows up with two dropdown boxes:

When I select an option in one box, that selection shows up in both boxes, and seems to be saved correctly in the results:

Any suggestions would be appreciated.

Two more data points to help try to resolve this:

  1. The issue persists whether the survey is added to the experiment by survey ID or by importing the survey JSON file
  2. The “extra” box at the top appears to have CSS class dropdown-mobile, while the “correct” box at the bottom has class sd.dropdown--empty

Thanks in advance for any help.

Even more debugging clues (but still no solution):

When the survey is embedded, there is an extra <select> component that is not there in the non-embedded survey:

<select data-name="question4" class="sd-input sd-dropdown dropdown-mobile">
  <option value=""></option>
  <option value="item1">item1</option>
  <option value="item2">item2</option>
  <option value="item3">item3</option>
</select>

In the non-embedded survey, that <select> is not present. The dropdown box in the non-embedded survey is only defined in the following <div> (which is also present in the embedded survey, just under the <select>:

<div data-bind="css: question.getControlClass(),
    click: click, 
    event: { keydown: keyhandler, blur: blur },
    attr: { 
      id: question.inputId, 
      required: question.isRequired, 
      tabindex: model.inputReadOnly ? undefined : 0,
      disabled: question.isInputReadOnly,
      role: question.ariaRole,
      'aria-required': question.ariaRequired, 
      'aria-label': question.ariaLabel, 
      'aria-invalid': question.ariaInvalid, 
      'aria-describedby': question.ariaDescribedBy 
    }," class="sd-input sd-dropdown sd-dropdown--empty" id="sq_100i" role="textbox" aria-required="false" aria-label="question72" aria-invalid="false">
    <div data-bind="css: question.koCss().controlValue" class="sd-dropdown__value">
      <input type="text" autocomplete="on" data-bind="
      textInput: model.filterString, 
      css: question.cssClasses.filterStringInput, 
      attr: { 
        placeholder: question.readOnlyText, 
        readonly: !model.searchEnabled, 
        tabindex: model.inputReadOnly ? undefined : -1,
        disabled: question.isInputReadOnly,
        id: question.getInputId() 
      },
      event: { blur: blur }" class="sd-dropdown__filter-string-input" placeholder="Select..." id="sq_100i_0">
    </div>
    <!-- ko if: (question.allowClear && question.cssClasses.cleanButtonIconId) -->
    <div data-bind="css: question.koCss().cleanButton, click: clear, visible: !question.isEmpty() " class="sd-dropdown_clean-button" style="display: none;">
      <!-- ko component: { name: 'sv-svg-icon', params: { css: question.cssClasses.cleanButtonSvg, iconName: question.cssClasses.cleanButtonIconId, size: 'auto', title: question.cleanButtonCaption } } --><!-- ko if: hasIcon -->
<svg class="sv-svg-icon sd-dropdown_clean-button-svg" data-bind="css: css, title: title, attr: { 'aria-label': title }" role="img" aria-label="Clean"><use xlink:href="#icon-clear"></use></svg>
<!-- /ko -->
<!-- /ko -->
    </div>
    <!-- /ko -->
  </div>

One more debugging update, but now I’m stuck.

The following is from /src/visual/survey/components/DropdownExtensions.js, starting line 16. The comment suggests that a <select> element is rendered only on mobile (on desktop it’s actually a <input> element), but it’s clearly showing up when embedded in an experiment, even on desktop.

function handleDropdownRendering (survey, options)
{
// Default SurveyJS drop down is actually an <input> with customly built options list
	// It works well on desktop, but not that convenient on mobile.
	// Adding native <select> here that's hidden by default but visible on mobile.

	const surveyCSS = options.question.css;
	const choices = options.question.getChoices();
	let optionsHTML = `<option value=""></option>`;
	let i;
	for (i = 0; i < choices.length; i++)
	{
		optionsHTML += `<option value="${choices[i].value}">${choices[i].text}</option>`;
	}
	const selectHTML = `<select data-name="${options.question.name}" class="${surveyCSS.dropdown.control} dropdown-mobile">${optionsHTML}</select>`;
	options.htmlElement.querySelector('.sd-selectbase').insertAdjacentHTML("beforebegin", selectHTML);

	const selectDOM = options.htmlElement.querySelector("select");
	selectDOM.addEventListener("change", handleValueChange.bind(this, survey, options));

	options.question.valueChangedCallback = handleValueChangeForDOM.bind(this, survey, options);
}

It adds the class dropdown-mobile to the select element, but so far as I can tell, that’s not defined in the css anywhere. Presumably dropdown-mobile should be set somewhere that if it’s on desktop it gets the css property display: none, but I don’t see that happening anywhere. (I would have thought it would be in lib/psychojs-2023.1.2.css but it doesn’t appear to be).

Any suggestions would be appreciated. If nothing else, is there a way for me to inject css to define dropdown-mobile to include display: none?

This is a bit of a hack, but for anyone stuck on this issue:

Using the UA-Parser JS library, you can figure out (based on user agent) whether the device is mobile or not.

In a code component:

Before experiment:

// Import user-agent parser library
import * as name from 'https://cdn.jsdelivr.net/npm/ua-parser-js/src/ua-parser.min.js';

Begin experiment:

var uap = new UAParser(); // get user-agent
var uap_device = uap.getDevice();

const addCSS = css => document.head.appendChild(document.createElement("style")).innerHTML=css; //function to add CSS

// If it's mobile or tablet, show the <select>; else show the <input>
if (uap_device.type == "mobile" || uap_device.type == "tablet" || uap_device.model == "iPad") {
    addCSS(`
       .sd-dropdown { display: none; }
        .dropdown-mobile { display: block; }
    `);
} else {
    addCSS(`
    .sd-dropdown { display: block; }
    .dropdown-mobile { display: none; }
    `)
}

I’d welcome a better solution, though.