Unity's dropdown system in 5.4.1 is unusable in production because it doesn't scroll the list to maintain view of the selected item when using the arrow keys. It also does not show the dropdown list on arrow key press, which is a standard behavior that should have been implemented. This script fixes that problem. One issue with my script is if the mouse is over the dropdown list, Unity will select the current item according to the current mouse position. This fights / overrides the selection from the arrow keys. The fix would be to disable the mouse on arrow key press, and reenable the mouse when the mouse is moved. This script doesn't do that, but that can be performed at a higher level. Code (CSharp): using UnityEngine; using System.Collections; using UnityEngine.UI; using UnityEngine.EventSystems; using System.Collections.Generic; public class DropdownAutoscroller : MonoBehaviour { [Tooltip("Assign to the dropdown that should automatically scroll according to the currently selected item.")] public Dropdown dropdown; // Use this for initialization void Start () { } // Update is called once per frame void Update () { if (EventSystem.current.currentSelectedGameObject == dropdown.gameObject) { if (Input.GetButtonUp("Vertical")) { Transform dropdownListTransform = dropdown.gameObject.transform.FindChild("Dropdown List"); if (dropdownListTransform == null) { // Show the dropdown when the user hits the arrow keys if the dropdown is not already showing dropdown.Show(); } } } else { PointerEventData eventDataCurrentPosition = new PointerEventData(EventSystem.current); eventDataCurrentPosition.position = new Vector2(Input.mousePosition.x, Input.mousePosition.y); List<RaycastResult> results = new List<RaycastResult>(); EventSystem.current.RaycastAll(eventDataCurrentPosition, results); if (results.Count > 0) { if (results[0].gameObject.transform.IsChildOf(dropdown.gameObject.transform)) { // Pointer over the list, use default behavior return; } } // Autoscroll list as the selected object is changed from the arrow keys if (EventSystem.current.currentSelectedGameObject.transform.IsChildOf(dropdown.gameObject.transform)) { if (EventSystem.current.currentSelectedGameObject.name.StartsWith("Item ")) { // Skip disabled items Transform parent = EventSystem.current.currentSelectedGameObject.transform.parent; int activeChildren = 0; int totalChildren = parent.childCount; for (int childIndex = 0; childIndex < totalChildren; childIndex++) { if (parent.GetChild(childIndex).gameObject.activeInHierarchy) { activeChildren++; } } int myActiveIndex = 0; for (int childIndex = 0; childIndex < totalChildren; childIndex++) { if (parent.GetChild(childIndex).gameObject == EventSystem.current.currentSelectedGameObject) { break; } else if (parent.GetChild(childIndex).gameObject.activeInHierarchy) { myActiveIndex++; } } if (activeChildren > 1) { GameObject scrollbarGameObject = GameObject.Find ("Scrollbar"); if (scrollbarGameObject != null && scrollbarGameObject.activeInHierarchy) { Scrollbar scrollbar = scrollbarGameObject.GetComponent<Scrollbar>(); if (scrollbar.direction == Scrollbar.Direction.TopToBottom) scrollbar.value = (float) myActiveIndex / (float) (activeChildren-1); else scrollbar.value = 1.0f - (float) myActiveIndex / (float) (activeChildren-1); } } } } } } }
I don't know it will work on your task or not. I simplified the above code and applied it the below way, and it worked fine on my project. Code (CSharp): using UnityEngine; using UnityEngine.EventSystems; using UnityEngine.UI; public class ExpandedDropdown : Dropdown { public override void OnPointerClick(PointerEventData eventData) { base.OnPointerClick(eventData); if (eventData.selectedObject.GetComponentInChildren<Scrollbar>() != null || !IsActive() || !IsInteractable()) return; Scrollbar scrollbar = gameObject.GetComponentInChildren<ScrollRect>()?.verticalScrollbar; if (options.Count > 1 && scrollbar != null) { if (scrollbar.direction == Scrollbar.Direction.TopToBottom) scrollbar.value = Mathf.Max(0.001f, (float) (value) / (float) (options.Count - 1)); else scrollbar.value = Mathf.Max(0.001f, 1.0f - (float) (value) / (float) (options.Count - 1)); } } }
Excuse me but how can I add this to an already existing TMP Dropdwon? I'm a bit of a begginner and I'm twinkering with UI. Thanks a bunch!
Is this logic expected to go to Unity's Dropdown classes? As RakNet I also thought it's pretty standard behavior, e.g., it's supported in HTML
Here's a script you can add (I modified VeryHappyYoung's code a bit) It depends on order of components on the GameObject so be sure to place this below actual Dropdown. Code (CSharp): using TMPro; using UnityEngine; using UnityEngine.EventSystems; using UnityEngine.UI; using static UnityEngine.UI.Scrollbar; /// <summary>Adds support for auto-scrolling to selected value</summary> /// <remarks>Place this after actual Dropdown Component, depends on GetComponents order logic</remarks> public class AutoscrollDropdown : MonoBehaviour, IPointerClickHandler { [SerializeField] private TMP_Dropdown target = null!; public void OnPointerClick(PointerEventData _) { if (!target.GetComponentInChildren<Scrollbar>() || !target.IsActive() || !target.IsInteractable()) { return; } var scrollbar = GetComponentInChildren<ScrollRect>()?.verticalScrollbar; if (target.options.Count > 1 && scrollbar != null) { var valuePosition = (float)target.value / (target.options.Count - 1); var value = scrollbar.direction == Direction.TopToBottom ? valuePosition : 1f - valuePosition; scrollbar.value = Mathf.Max(.001f, value); } } } BTW, @VeryHappyYoung, could you explain eventData.selectedObject.GetComponentInChildren<Scrollbar>() != null logic (why != null, not == null)? It didn't work for me (and also thrown NullReferenceException when nothing was selected yet) so I changed it to !target.GetComponentInChildren<Scrollbar>()
I came up with this method. Basically you have the items take their child index and divide it by the siblings to get an approximation of what the scroll value should be. Then when a child object is selected, you set the scrollbar's value to that number.