// Copyright (c) MudBlazor 2021 // MudBlazor licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. using System.Diagnostics.CodeAnalysis; using Connected.Annotations; using Connected.Services; using Connected.Utilities; using Connected.Utilities.Exceptions; using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components.Web; namespace Connected.Components; public partial class Select : InputBase, ISelect, IShadowSelect { private HashSet _selectedValues = new HashSet(); private IEqualityComparer _comparer; private bool _dense; private string multiSelectionText; private bool? _selectAllChecked; private IKeyInterceptor _keyInterceptor; protected string Classname => new CssBuilder("mud-select") .AddClass(Class) .Build(); [Inject] private IKeyInterceptorFactory KeyInterceptorFactory { get; set; } [Inject] IScrollManager ScrollManager { get; set; } private string _elementId = "select_" + Guid.NewGuid().ToString().Substring(0, 8); private Task SelectNextItem() => SelectAdjacentItem(+1); private Task SelectPreviousItem() => SelectAdjacentItem(-1); private async Task SelectAdjacentItem(int direction) { if (_items == null || _items.Count == 0) return; var index = _items.FindIndex(x => x.ItemId == (string)_activeItemId); if (direction < 0 && index < 0) index = 0; SelectItem item = null; // the loop allows us to jump over disabled items until we reach the next non-disabled one for (int i = 0; i < _items.Count; i++) { index += direction; if (index < 0) index = 0; if (index >= _items.Count) index = _items.Count - 1; if (_items[index].Disabled) continue; item = _items[index]; if (!MultiSelection) { _selectedValues.Clear(); _selectedValues.Add(item.Value); await SetValueAsync(item.Value, updateText: true); HilightItem(item); break; } else { // in multiselect mode don't select anything, just hilight. // selecting is done by Enter HilightItem(item); break; } } await _elementReference.SetText(Text); await ScrollToItemAsync(item); } private ValueTask ScrollToItemAsync(SelectItem item) => item != null ? ScrollManager.ScrollToListItemAsync(item.ItemId) : ValueTask.CompletedTask; private async Task SelectFirstItem(string startChar = null) { if (_items == null || _items.Count == 0) return; var items = _items.Where(x => !x.Disabled); var firstItem = items.FirstOrDefault(); if (!string.IsNullOrWhiteSpace(startChar)) { // find first item that starts with the letter var currentItem = items.FirstOrDefault(x => x.ItemId == (string)_activeItemId); if (currentItem != null && Converter.Set(currentItem.Value)?.ToLowerInvariant().StartsWith(startChar) == true) { // this will step through all items that start with the same letter if pressed multiple times items = items.SkipWhile(x => x != currentItem).Skip(1); } items = items.Where(x => Converter.Set(x.Value)?.ToLowerInvariant().StartsWith(startChar) == true); } var item = items.FirstOrDefault(); if (item == null) return; if (!MultiSelection) { _selectedValues.Clear(); _selectedValues.Add(item.Value); await SetValueAsync(item.Value, updateText: true); HilightItem(item); } else { HilightItem(item); } await _elementReference.SetText(Text); await ScrollToItemAsync(item); } private async Task SelectLastItem() { if (_items == null || _items.Count == 0) return; var item = _items.LastOrDefault(x => !x.Disabled); if (item == null) return; if (!MultiSelection) { _selectedValues.Clear(); _selectedValues.Add(item.Value); await SetValueAsync(item.Value, updateText: true); HilightItem(item); } else { HilightItem(item); } await _elementReference.SetText(Text); await ScrollToItemAsync(item); } /// /// Fired when dropdown opens. /// [Category(CategoryTypes.FormComponent.Behavior)] [Parameter] public EventCallback OnOpen { get; set; } /// /// Fired when dropdown closes. /// [Category(CategoryTypes.FormComponent.Behavior)] [Parameter] public EventCallback OnClose { get; set; } /// /// Add the MudSelectItems here /// [Parameter] [Category(CategoryTypes.FormComponent.ListBehavior)] public RenderFragment ChildContent { get; set; } /// /// User class names for the popover, separated by space /// [Parameter] [Category(CategoryTypes.FormComponent.ListAppearance)] public string PopoverClass { get; set; } /// /// If true, compact vertical padding will be applied to all Select items. /// [Parameter] [Category(CategoryTypes.FormComponent.ListAppearance)] public bool Dense { get { return _dense; } set { _dense = value; } } /// /// The Open Select Icon /// [Parameter] [Category(CategoryTypes.FormComponent.Appearance)] public string OpenIcon { get; set; } = Icons.Material.Filled.ArrowDropDown; /// /// The Close Select Icon /// [Parameter] [Category(CategoryTypes.FormComponent.Appearance)] public string CloseIcon { get; set; } = Icons.Material.Filled.ArrowDropUp; /// /// If set to true and the MultiSelection option is set to true, a "select all" checkbox is added at the top of the list of items. /// [Parameter] [Category(CategoryTypes.FormComponent.ListBehavior)] public bool SelectAll { get; set; } /// /// Define the text of the Select All option. /// [Parameter] [Category(CategoryTypes.FormComponent.ListAppearance)] public string SelectAllText { get; set; } = "Select all"; /// /// Fires when SelectedValues changes. /// [Parameter] public EventCallback> SelectedValuesChanged { get; set; } /// /// Function to define a customized multiselection text. /// [Parameter] [Category(CategoryTypes.FormComponent.Behavior)] public Func, string> MultiSelectionTextFunc { get; set; } /// /// Parameter to define the delimited string separator. /// [Parameter] [Category(CategoryTypes.FormComponent.Behavior)] public string Delimiter { get; set; } = ", "; /// /// Set of selected values. If MultiSelection is false it will only ever contain a single value. This property is two-way bindable. /// [Parameter] [Category(CategoryTypes.FormComponent.Data)] public IEnumerable SelectedValues { get { if (_selectedValues == null) _selectedValues = new HashSet(_comparer); return _selectedValues; } set { var set = value ?? new HashSet(_comparer); if (SelectedValues.Count() == set.Count() && _selectedValues.All(x => set.Contains(x))) return; _selectedValues = new HashSet(set, _comparer); SelectionChangedFromOutside?.Invoke(_selectedValues); if (!MultiSelection) SetValueAsync(_selectedValues.FirstOrDefault()).AndForget(); else { //Warning. Here the Converter was not set yet if (MultiSelectionTextFunc != null) { SetCustomizedTextAsync(string.Join(Delimiter, SelectedValues.Select(x => Converter.Set(x))), selectedConvertedValues: SelectedValues.Select(x => Converter.Set(x)).ToList(), multiSelectionTextFunc: MultiSelectionTextFunc).AndForget(); } else { SetTextAsync(string.Join(Delimiter, SelectedValues.Select(x => Converter.Set(x))), updateValue: false).AndForget(); } } SelectedValuesChanged.InvokeAsync(new HashSet(SelectedValues, _comparer)); if (MultiSelection && typeof(T) == typeof(string)) SetValueAsync((T)(object)Text, updateText: false).AndForget(); } } /// /// The Comparer to use for comparing selected values internally. /// [Parameter] [Category(CategoryTypes.FormComponent.Behavior)] public IEqualityComparer Comparer { get => _comparer; set { _comparer = value; // Apply comparer and refresh selected values _selectedValues = new HashSet(_selectedValues, _comparer); SelectedValues = _selectedValues; } } private Func _toStringFunc = x => x?.ToString(); private Input _elementReference; /// /// Defines how values are displayed in the drop-down list /// [Parameter] [Category(CategoryTypes.FormComponent.ListBehavior)] public Func ToStringFunc { get => _toStringFunc; set { if (_toStringFunc == value) return; _toStringFunc = value; Converter = new Converter { SetFunc = _toStringFunc ?? (x => x?.ToString()), //GetFunc = LookupValue, }; } } public Select() { Adornment = Adornment.End; IconSize = Size.Medium; } protected override void OnAfterRender(bool firstRender) { base.OnAfterRender(firstRender); if (firstRender && Value != null) { // we need to render the initial Value which is not possible without the items // which supply the RenderFragment. So in this case, a second render is necessary StateHasChanged(); } UpdateSelectAllChecked(); lock (this) { if (_renderComplete != null) { _renderComplete.TrySetResult(); _renderComplete = null; } } } private Task WaitForRender() { Task t = null; lock (this) { if (_renderComplete != null) return _renderComplete.Task; _renderComplete = new TaskCompletionSource(); t = _renderComplete.Task; } StateHasChanged(); return t; } private TaskCompletionSource _renderComplete; /// /// Returns whether or not the Value can be found in items. If not, the Select will display it as a string. /// protected bool CanRenderValue { get { if (Value == null) return false; if (!_shadowLookup.TryGetValue(Value, out var item)) return false; return (item.ChildContent != null); } } protected bool IsValueInList { get { if (Value == null) return false; return _shadowLookup.TryGetValue(Value, out var _); } } protected RenderFragment GetSelectedValuePresenter() { if (Value == null) return null; if (!_shadowLookup.TryGetValue(Value, out var item)) return null; //<-- for now. we'll add a custom template to present values (set from outside) which are not on the list? return item.ChildContent; } protected override Task UpdateValuePropertyAsync(bool updateText) { // For MultiSelection of non-string T's we don't update the Value!!! if (typeof(T) == typeof(string) || !MultiSelection) base.UpdateValuePropertyAsync(updateText); return Task.CompletedTask; } protected override Task UpdateTextPropertyAsync(bool updateValue) { // when multiselection is true, we return // a comma separated list of selected values if (MultiSelectionTextFunc != null) { return MultiSelection ? SetCustomizedTextAsync(string.Join(Delimiter, SelectedValues.Select(x => Converter.Set(x))), selectedConvertedValues: SelectedValues.Select(x => Converter.Set(x)).ToList(), multiSelectionTextFunc: MultiSelectionTextFunc) : base.UpdateTextPropertyAsync(updateValue); } else { return MultiSelection ? SetTextAsync(string.Join(Delimiter, SelectedValues.Select(x => Converter.Set(x)))) : base.UpdateTextPropertyAsync(updateValue); } } internal event Action> SelectionChangedFromOutside; private bool _multiSelection; /// /// If true, multiple values can be selected via checkboxes which are automatically shown in the dropdown /// [Parameter] [Category(CategoryTypes.FormComponent.ListBehavior)] public bool MultiSelection { get => _multiSelection; set { if (value != _multiSelection) { _multiSelection = value; UpdateTextPropertyAsync(false).AndForget(); } } } /// /// The collection of items within this select /// public IReadOnlyList> Items => _items; protected internal List> _items = new(); protected Dictionary> _valueLookup = new(); protected Dictionary> _shadowLookup = new(); // note: this must be object to satisfy MudList private object _activeItemId = null; internal bool Add(SelectItem item) { if (item == null) return false; bool? result = null; if (!_items.Select(x => x.Value).Contains(item.Value)) { _items.Add(item); if (item.Value != null) { _valueLookup[item.Value] = item; if (item.Value.Equals(Value) && !MultiSelection) result = true; } } UpdateSelectAllChecked(); if (result.HasValue == false) { result = item.Value?.Equals(Value); } return result == true; } internal void Remove(SelectItem item) { _items.Remove(item); if (item.Value != null) _valueLookup.Remove(item.Value); } /// /// Sets the maxheight the Select can have when open. /// [Parameter] [Category(CategoryTypes.FormComponent.ListAppearance)] public int MaxHeight { get; set; } = 300; /// /// Set the anchor origin point to determen where the popover will open from. /// [Parameter] [Category(CategoryTypes.FormComponent.ListAppearance)] public Origin AnchorOrigin { get; set; } = Origin.TopCenter; /// /// Sets the transform origin point for the popover. /// [Parameter] [Category(CategoryTypes.FormComponent.ListAppearance)] public Origin TransformOrigin { get; set; } = Origin.TopCenter; /// /// Sets the direction the Select menu should open. /// [ExcludeFromCodeCoverage] [Obsolete("Use AnchorOrigin or TransformOrigin instead.", true)] [Parameter] public Direction Direction { get; set; } = Direction.Bottom; /// /// If true, the Select menu will open either before or after the input (left/right). /// [ExcludeFromCodeCoverage] [Obsolete("Use AnchorOrigin or TransformOrigin instead.", true)] [Parameter] public bool OffsetX { get; set; } /// /// If true, the Select menu will open either before or after the input (top/bottom). /// /// [ExcludeFromCodeCoverage] [Obsolete("Use AnchorOrigin or TransformOrigin instead.", true)] [Parameter] public bool OffsetY { get; set; } /// /// If true, the Select's input will not show any values that are not defined in the dropdown. /// This can be useful if Value is bound to a variable which is initialized to a value which is not in the list /// and you want the Select to show the label / placeholder instead. /// [Parameter] [Category(CategoryTypes.FormComponent.Behavior)] public bool Strict { get; set; } /// /// Show clear button. /// [Parameter] [Category(CategoryTypes.FormComponent.Behavior)] public bool Clearable { get; set; } = false; /// /// If true, prevent scrolling while dropdown is open. /// [Parameter] [Category(CategoryTypes.FormComponent.ListBehavior)] public bool LockScroll { get; set; } = false; /// /// Button click event for clear button. Called after text and value has been cleared. /// [Parameter] public EventCallback OnClearButtonClick { get; set; } internal bool _isOpen; public string _currentIcon { get; set; } public async Task SelectOption(int index) { if (index < 0 || index >= _items.Count) { if (!MultiSelection) await CloseMenu(); return; } await SelectOption(_items[index].Value); } public async Task SelectOption(object obj) { var value = (T)obj; if (MultiSelection) { // multi-selection: menu stays open if (!_selectedValues.Contains(value)) _selectedValues.Add(value); else _selectedValues.Remove(value); if (MultiSelectionTextFunc != null) { await SetCustomizedTextAsync(string.Join(Delimiter, SelectedValues.Select(x => Converter.Set(x))), selectedConvertedValues: SelectedValues.Select(x => Converter.Set(x)).ToList(), multiSelectionTextFunc: MultiSelectionTextFunc); } else { await SetTextAsync(string.Join(Delimiter, SelectedValues.Select(x => Converter.Set(x))), updateValue: false); } UpdateSelectAllChecked(); BeginValidate(); } else { // single selection // CloseMenu(true) doesn't close popover in BSS await CloseMenu(false); if (EqualityComparer.Default.Equals(Value, value)) { StateHasChanged(); return; } await SetValueAsync(value); _elementReference.SetText(Text).AndForget(); _selectedValues.Clear(); _selectedValues.Add(value); } HilightItemForValue(value); await SelectedValuesChanged.InvokeAsync(SelectedValues); if (MultiSelection && typeof(T) == typeof(string)) await SetValueAsync((T)(object)Text, updateText: false); await InvokeAsync(StateHasChanged); } private async void HilightItemForValue(T value) { if (value == null) { HilightItem(null); return; } await WaitForRender(); _valueLookup.TryGetValue(value, out var item); HilightItem(item); } private async void HilightItem(SelectItem item) { _activeItemId = item?.ItemId; // we need to make sure we are just after a render here or else there will be race conditions await WaitForRender(); // Note: this is a hack but I found no other way to make the list hilight the currently hilighted item // without the delay it always shows the previously hilighted item because the popup items don't exist yet // they are only registered after they are rendered, so we need to render again! await Task.Delay(1); StateHasChanged(); } private async Task HilightSelectedValue() { await WaitForRender(); if (MultiSelection) HilightItem(_items.FirstOrDefault(x => !x.Disabled)); else HilightItemForValue(Value); } private void UpdateSelectAllChecked() { if (MultiSelection && SelectAll) { var oldState = _selectAllChecked; if (_selectedValues.Count == 0) { _selectAllChecked = false; } else if (_items.Count == _selectedValues.Count) { _selectAllChecked = true; } else { _selectAllChecked = null; } } } public async Task ToggleMenu() { if (Disabled || ReadOnly) return; if (_isOpen) await CloseMenu(true); else await OpenMenu(); } public async Task OpenMenu() { if (Disabled || ReadOnly) return; _isOpen = true; UpdateIcon(); StateHasChanged(); await HilightSelectedValue(); //Scroll the active item on each opening if (_activeItemId != null) { var index = _items.FindIndex(x => x.ItemId == (string)_activeItemId); if (index > 0) { var item = _items[index]; await ScrollToItemAsync(item); } } //disable escape propagation: if selectmenu is open, only the select popover should close and underlying components should not handle escape key await _keyInterceptor.UpdateKey(new() { Key = "Escape", StopDown = "Key+none" }); await OnOpen.InvokeAsync(); } public async Task CloseMenu(bool focusAgain = true) { _isOpen = false; UpdateIcon(); if (focusAgain == true) { StateHasChanged(); await OnBlur.InvokeAsync(new FocusEventArgs()); _elementReference.FocusAsync().AndForget(TaskOption.Safe); StateHasChanged(); } //enable escape propagation: the select popover was closed, now underlying components are allowed to handle escape key await _keyInterceptor.UpdateKey(new() { Key = "Escape", StopDown = "none" }); await OnClose.InvokeAsync(); } private void UpdateIcon() { _currentIcon = !string.IsNullOrWhiteSpace(AdornmentIcon) ? AdornmentIcon : _isOpen ? CloseIcon : OpenIcon; } protected override void OnInitialized() { base.OnInitialized(); UpdateIcon(); } protected override void OnParametersSet() { base.OnParametersSet(); UpdateIcon(); } protected override async Task OnAfterRenderAsync(bool firstRender) { if (firstRender) { _keyInterceptor = KeyInterceptorFactory.Create(); await _keyInterceptor.Connect(_elementId, new KeyInterceptorOptions() { //EnableLogging = true, TargetClass = "mud-input-control", Keys = { new KeyOptions { Key=" ", PreventDown = "key+none" }, //prevent scrolling page, toggle open/close new KeyOptions { Key="ArrowUp", PreventDown = "key+none" }, // prevent scrolling page, instead hilight previous item new KeyOptions { Key="ArrowDown", PreventDown = "key+none" }, // prevent scrolling page, instead hilight next item new KeyOptions { Key="Home", PreventDown = "key+none" }, new KeyOptions { Key="End", PreventDown = "key+none" }, new KeyOptions { Key="Escape" }, new KeyOptions { Key="Enter", PreventDown = "key+none" }, new KeyOptions { Key="NumpadEnter", PreventDown = "key+none" }, new KeyOptions { Key="a", PreventDown = "key+ctrl" }, // select all items instead of all page text new KeyOptions { Key="A", PreventDown = "key+ctrl" }, // select all items instead of all page text new KeyOptions { Key="/./", SubscribeDown = true, SubscribeUp = true }, // for our users }, }); _keyInterceptor.KeyDown += HandleKeyDown; _keyInterceptor.KeyUp += HandleKeyUp; } await base.OnAfterRenderAsync(firstRender); } public void CheckGenericTypeMatch(object select_item) { var itemT = select_item.GetType().GenericTypeArguments[0]; if (itemT != typeof(T)) throw new GenericTypeMismatchException("MudSelect", "MudSelectItem", typeof(T), itemT); } public override ValueTask FocusAsync() { return _elementReference.FocusAsync(); } public override ValueTask BlurAsync() { return _elementReference.BlurAsync(); } public override ValueTask SelectAsync() { return _elementReference.SelectAsync(); } public override ValueTask SelectRangeAsync(int pos1, int pos2) { return _elementReference.SelectRangeAsync(pos1, pos2); } /// /// Extra handler for clearing selection. /// protected async ValueTask SelectClearButtonClickHandlerAsync(MouseEventArgs e) { await SetValueAsync(default, false); await SetTextAsync(default, false); _selectedValues.Clear(); BeginValidate(); StateHasChanged(); await SelectedValuesChanged.InvokeAsync(_selectedValues); await OnClearButtonClick.InvokeAsync(e); } protected async Task SetCustomizedTextAsync(string text, bool updateValue = true, List selectedConvertedValues = null, Func, string> multiSelectionTextFunc = null) { // The Text property of the control is updated Text = multiSelectionTextFunc?.Invoke(selectedConvertedValues); // The comparison is made on the multiSelectionText variable if (multiSelectionText != text) { multiSelectionText = text; if (!string.IsNullOrWhiteSpace(multiSelectionText)) Touched = true; if (updateValue) await UpdateValuePropertyAsync(false); await TextChanged.InvokeAsync(multiSelectionText); } } /// /// Custom checked icon. /// [Parameter] [Category(CategoryTypes.FormComponent.ListAppearance)] public string CheckedIcon { get; set; } = Icons.Material.Filled.CheckBox; /// /// Custom unchecked icon. /// [Parameter] [Category(CategoryTypes.FormComponent.ListAppearance)] public string UncheckedIcon { get; set; } = Icons.Material.Filled.CheckBoxOutlineBlank; /// /// Custom indeterminate icon. /// [Parameter] [Category(CategoryTypes.FormComponent.ListAppearance)] public string IndeterminateIcon { get; set; } = Icons.Material.Filled.IndeterminateCheckBox; /// /// The checkbox icon reflects the select all option's state /// protected string SelectAllCheckBoxIcon { get { return _selectAllChecked.HasValue ? _selectAllChecked.Value ? CheckedIcon : UncheckedIcon : IndeterminateIcon; } } internal async void HandleKeyDown(KeyboardEventArgs obj) { if (Disabled || ReadOnly) return; var key = obj.Key.ToLowerInvariant(); if (_isOpen && key.Length == 1 && key != " " && !(obj.CtrlKey || obj.ShiftKey || obj.AltKey || obj.MetaKey)) { await SelectFirstItem(key); return; } switch (obj.Key) { case "Tab": await CloseMenu(false); break; case "ArrowUp": if (obj.AltKey == true) { await CloseMenu(); break; } else if (_isOpen == false) { await OpenMenu(); break; } else { await SelectPreviousItem(); break; } case "ArrowDown": if (obj.AltKey == true) { await OpenMenu(); break; } else if (_isOpen == false) { await OpenMenu(); break; } else { await SelectNextItem(); break; } case " ": await ToggleMenu(); break; case "Escape": await CloseMenu(true); break; case "Home": await SelectFirstItem(); break; case "End": await SelectLastItem(); break; case "Enter": case "NumpadEnter": var index = _items.FindIndex(x => x.ItemId == (string)_activeItemId); if (!MultiSelection) { if (!_isOpen) { await OpenMenu(); return; } // this also closes the menu await SelectOption(index); break; } else { if (_isOpen == false) { await OpenMenu(); break; } else { await SelectOption(index); await _elementReference.SetText(Text); break; } } case "a": case "A": if (obj.CtrlKey == true) { if (MultiSelection) { await SelectAllClickAsync(); //If we didn't add delay, it won't work. await WaitForRender(); await Task.Delay(1); StateHasChanged(); //It only works when selecting all, not render unselect all. //UpdateSelectAllChecked(); } } break; } OnKeyDown.InvokeAsync(obj).AndForget(); } internal void HandleKeyUp(KeyboardEventArgs obj) { OnKeyUp.InvokeAsync(obj).AndForget(); } [ExcludeFromCodeCoverage] [Obsolete("Use Clear instead.", true)] public Task ClearAsync() => Clear(); /// /// Clear the selection /// public async Task Clear() { await SetValueAsync(default, false); await SetTextAsync(default, false); _selectedValues.Clear(); BeginValidate(); StateHasChanged(); await SelectedValuesChanged.InvokeAsync(_selectedValues); } private async Task SelectAllClickAsync() { // Manage the fake tri-state of a checkbox if (!_selectAllChecked.HasValue) _selectAllChecked = true; else if (_selectAllChecked.Value) _selectAllChecked = false; else _selectAllChecked = true; // Define the items selection if (_selectAllChecked.Value == true) await SelectAllItems(); else await Clear(); } private async Task SelectAllItems() { if (!MultiSelection) return; var selectedValues = new HashSet(_items.Where(x => !x.Disabled && x.Value != null).Select(x => x.Value), _comparer); _selectedValues = new HashSet(selectedValues, _comparer); if (MultiSelectionTextFunc != null) { await SetCustomizedTextAsync(string.Join(Delimiter, SelectedValues.Select(x => Converter.Set(x))), selectedConvertedValues: SelectedValues.Select(x => Converter.Set(x)).ToList(), multiSelectionTextFunc: MultiSelectionTextFunc); } else { await SetTextAsync(string.Join(Delimiter, SelectedValues.Select(x => Converter.Set(x))), updateValue: false); } UpdateSelectAllChecked(); _selectedValues = selectedValues; // need to force selected values because Blazor overwrites it under certain circumstances due to changes of Text or Value BeginValidate(); await SelectedValuesChanged.InvokeAsync(SelectedValues); if (MultiSelection && typeof(T) == typeof(string)) SetValueAsync((T)(object)Text, updateText: false).AndForget(); } public void RegisterShadowItem(SelectItem item) { if (item == null || item.Value == null) return; _shadowLookup[item.Value] = item; } public void UnregisterShadowItem(SelectItem item) { if (item == null || item.Value == null) return; _shadowLookup.Remove(item.Value); } internal void OnLostFocus(FocusEventArgs obj) { if (_isOpen) { // when the menu is open we immediately get back the focus if we lose it (i.e. because of checkboxes in multi-select) // otherwise we can't receive key strokes any longer _elementReference.FocusAsync().AndForget(TaskOption.Safe); } base.OnBlur.InvokeAsync(obj); } protected override void Dispose(bool disposing) { base.Dispose(disposing); if (disposing == true) { if (_keyInterceptor != null) { _keyInterceptor.KeyDown -= HandleKeyDown; _keyInterceptor.KeyUp -= HandleKeyUp; _keyInterceptor.Dispose(); } } } /// /// Fixes issue #4328 /// Returns true when MultiSelection is true and it has selected values(Since Value property is not used when MultiSelection=true /// /// /// True when component has a value protected override bool HasValue(T value) { if (MultiSelection) return SelectedValues?.Count() > 0; else return base.HasValue(value); } }