Skip to content
This repository has been archived by the owner on May 1, 2024. It is now read-only.

Commit

Permalink
Add TabStop/TabIndex (#2795)
Browse files Browse the repository at this point in the history
* [Core, UWP, Android] support TabStop/TabIndex

* removing linq

* add iOS / MacOS implementation

* add WPF implementation
fixes UWP implementation

* - addressing comments
- improve test
- optimizations
- added previous tab direction in same tab group

* addressing comments

* [Android] support tabstop for pickers

* - moving shared code to a static class
- removed reflection

* ITabStop -> changed to internal, renamed, added in to fastRenderers

* Update ITabStop to be public
  • Loading branch information
Pavel Yakovlev authored and Jason Smith committed Sep 13, 2018
1 parent ad8ac34 commit cceee9d
Show file tree
Hide file tree
Showing 25 changed files with 735 additions and 59 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,244 @@
using System.Collections.Generic;
using Xamarin.Forms.CustomAttributes;
using Xamarin.Forms.Internals;

namespace Xamarin.Forms.Controls.Issues
{
[Preserve(AllMembers = true)]
[Issue(IssueTracker.Github, 1700, "Desktop: TabStop/TabIndex support (for multiple Views)", PlatformAffected.All)]
public class GitHub1700 : TestContentPage
{
IList<View> listViews;

void IndexDesc()
{
int index = 100500;
foreach (var item in listViews)
{
if (item is Button but && but.Text.StartsWith("TabIndex"))
continue;
item.TabIndex = index--;
}
}

void IndexNegative()
{
int index = -100;
foreach (var item in listViews)
{
if (item is Button but && but.Text.StartsWith("TabIndex"))
continue;
item.TabIndex = index++;
}
}

protected override void Init()
{
var actionGrid = new Grid()
{
Padding = new Thickness(10),
BackgroundColor = Color.Aquamarine
};
actionGrid.AddChild(new Button()
{
Text = "Index desc",
Command = new Command(() => IndexDesc())
}, 0, 0);
actionGrid.AddChild(new Button()
{
Text = "All indexes equal 0",
Command = new Command(() => listViews.ForEach(c => c.TabIndex = 0))
}, 1, 0);
actionGrid.AddChild(new Button()
{
Text = "Negative indexes",
Command = new Command(() => IndexNegative())
}, 2, 0);
actionGrid.AddChild(new Button()
{
Text = "TabStops = True",
Command = new Command(() => listViews.ForEach(c => c.IsTabStop = true))
}, 0, 1);
actionGrid.AddChild(new Button()
{
Text = "TabStops = False",
Command = new Command(() => listViews.ForEach(c => c.IsTabStop = false))
}, 1, 1);
actionGrid.AddChild(new Button()
{
Text = "TabStops every second",
Command = new Command(() =>
{
for (int i = 0; i < listViews.Count; i++)
listViews[i].IsTabStop = i % 2 == 0;
})
}, 2, 1);

var pickerStopped = new Picker
{
Title = $"[+] Picker - Tab stop enable",
IsTabStop = true
};
var pickerNotStopped = new Picker
{
Title = "[-] Picker - Tab stop disable",
IsTabStop = false
};
for (var i = 1; i < 3; i++) {
pickerNotStopped.Items.Add("Sample Option " + i);
pickerStopped.Items.Add("Sample Option " + i);
}

var stack = new StackLayout
{
Children =
{
actionGrid,
pickerStopped,
pickerNotStopped,
new Button
{
Text = $"TabIndex 90",
IsTabStop = true,
TabIndex = 90
},
new Button
{
Text = $"TabIndex 100",
IsTabStop = true,
TabIndex = 100
},
new Button
{
Text = $"TabIndex 100",
IsTabStop = true,
TabIndex = 100
},
new Button
{
Text = $"TabIndex 90",
IsTabStop = true,
TabIndex = 90
},
new Button
{
Text = $"[+] Button - TabStop enable",
IsTabStop = true
},
new Button
{
Text = "Button - Non stop",
IsTabStop = false
},
new DatePicker
{
IsTabStop = true
},
new DatePicker
{
IsTabStop = false
},
new Editor
{
Text = $"[+] Editor - Tab stop enable",
IsTabStop = true
},
new Editor
{
Text = "Editor - Non stop",
IsTabStop = false
},
new Entry
{
Text = $"[+] Entry - Tab stop enable",
IsTabStop = true
},
new Entry
{
Text = "Entry - Non stop",
IsTabStop = false
},
new ProgressBar
{
IsTabStop = true,
HeightRequest = 40,
Progress = 80
},
new ProgressBar
{
IsTabStop = false,
HeightRequest = 40,
Progress = 40
},
new SearchBar
{
Text = $"[+] SearchBar - TabStop enable",
IsTabStop = true
},
new SearchBar
{
Text = "SearchBar - TabStop disable",
IsTabStop = false
},
new Slider
{
IsTabStop = true
},
new Slider
{
IsTabStop = false
},
new Stepper
{
IsTabStop = true
},
new Stepper
{
IsTabStop = false
},
new Switch
{
IsTabStop = true
},
new Switch
{
IsTabStop = false
},
new TimePicker
{
IsTabStop = true
},
new TimePicker
{
IsTabStop = false
},
}
};

listViews = stack.Children;

foreach (var item in listViews)
{
item.Focused += (_, e) =>
{
BackgroundColor = e.VisualElement.IsTabStop ? Color.Transparent : Color.OrangeRed;
Title = $"{e.VisualElement.TabIndex} - " + (e.VisualElement.IsTabStop ? "[+]" : "WRONG");
e.VisualElement.Scale = 0.7;
};
item.Unfocused += (_, e) =>
{
BackgroundColor = Color.Transparent;
Title = string.Empty;
e.VisualElement.Scale = 1;
};
}

IndexDesc();

Content = new ScrollView()
{
Content = stack
};
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,7 @@
<Compile Include="$(MSBuildThisFileDirectory)Effects\AttachedStateEffectList.cs" />
<Compile Include="$(MSBuildThisFileDirectory)GitHub1648.cs" />
<Compile Include="$(MSBuildThisFileDirectory)GitHub1702.cs" />
<Compile Include="$(MSBuildThisFileDirectory)GitHub1700.cs" />
<Compile Include="$(MSBuildThisFileDirectory)GitHub2598.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Issue1483.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Issue1556.cs" />
Expand Down
14 changes: 3 additions & 11 deletions Xamarin.Forms.Controls/TestCases.cs
Original file line number Diff line number Diff line change
Expand Up @@ -242,19 +242,11 @@ from issueModel in _issues
Root.Add(_section);
}

HashSet<string> _exemptNames = new HashSet<string> { "N0", "G342", "G1305", "G1461", "G1653", "G1700" };

// Legacy reasons, do not add to this list
// Going forward, make sure only one Issue attribute exist for a Tracker + Issue number pair
bool IsExempt (string name)
{
if (name == "G1461" ||
name == "G342" ||
name == "G1305" ||
name == "G1653" ||
name == "N0")
return true;
else
return false;
}
bool IsExempt(string name) => _exemptNames.Contains(name);
}

public static NavigationPage GetTestCases ()
Expand Down
14 changes: 14 additions & 0 deletions Xamarin.Forms.Core/EnumerableExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,20 @@ public static void ForEach<T>(this IEnumerable<T> enumeration, Action<T> action)
}
}

public static IDictionary<TKey, List<TSource>> GroupToDictionary<TSource, TKey>(this IEnumerable<TSource> enumeration, Func<TSource, TKey> func)
{
var result = new Dictionary<TKey, List<TSource>>();
foreach (TSource item in enumeration)
{
var group = func(item);
if (!result.ContainsKey(group))
result.Add(group, new List<TSource> { item });
else
result[group].Add(item);
}
return result;
}

public static int IndexOf<T>(this IEnumerable<T> enumerable, T item)
{
if (enumerable == null)
Expand Down
2 changes: 2 additions & 0 deletions Xamarin.Forms.Core/ProgressBar.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ public class ProgressBar : View, IElementConfiguration<ProgressBar>

public static readonly BindableProperty ProgressProperty = BindableProperty.Create(nameof(Progress), typeof(double), typeof(ProgressBar), 0d, coerceValue: (bo, v) => ((double)v).Clamp(0, 1));

protected override bool TabStopDefaultValueCreator() => false;

readonly Lazy<PlatformConfigurationRegistry<ProgressBar>> _platformConfigurationRegistry;

public ProgressBar()
Expand Down
84 changes: 84 additions & 0 deletions Xamarin.Forms.Core/TabIndexExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
using System.Collections.Generic;
using Xamarin.Forms.Internals;

namespace Xamarin.Forms
{
public static class TabIndexExtensions
{
public static IDictionary<int, List<VisualElement>> GetTabIndexesOnParentPage(this VisualElement element, out int countChildrensWithTabStopWithoutThis)
{
countChildrensWithTabStopWithoutThis = 0;

Element parentPage = element.Parent;
while (parentPage != null && !(parentPage is Page))
parentPage = parentPage.Parent;

var descendantsOnPage = parentPage?.VisibleDescendants();
if (descendantsOnPage == null)
return null;

var childrensWithTabStop = new List<VisualElement>();
foreach (var descendant in descendantsOnPage)
{
if (descendant is VisualElement visualElement && visualElement.IsTabStop)
childrensWithTabStop.Add(visualElement);
}
if (!childrensWithTabStop.Contains(element))
return null;

countChildrensWithTabStopWithoutThis = childrensWithTabStop.Count - 1;
return childrensWithTabStop.GroupToDictionary(c => c.TabIndex);
}

public static VisualElement FindNextElement(this VisualElement element, bool forwardDirection, IDictionary<int, List<VisualElement>> tabIndexes, ref int tabIndex)
{
var tabGroup = tabIndexes[tabIndex];
if (!forwardDirection)
{
// search prev element in same TabIndex group
var prevSubIndex = tabGroup.IndexOf(element) - 1;
if (prevSubIndex >= 0 && prevSubIndex < tabGroup.Count)
{
return tabGroup[prevSubIndex];
}
else // search prev element in prev TabIndex group
{
var smallerMax = int.MinValue;
var tabIndexesMax = int.MinValue;
foreach (var index in tabIndexes.Keys)
{
if (index < tabIndex && smallerMax < index)
smallerMax = index;
if (tabIndexesMax < index)
tabIndexesMax = index;
}
tabIndex = smallerMax != int.MinValue ? smallerMax : tabIndexesMax;
return tabIndexes[tabIndex][0];
}
}
else // Forward
{
// search next element in same TabIndex group
var nextSubIndex = tabGroup.IndexOf(element) + 1;
if (nextSubIndex > 0 && nextSubIndex < tabGroup.Count)
{
return tabGroup[nextSubIndex];
}
else // search next element in next TabIndex group
{
var biggerMin = int.MaxValue;
var tabIndexesMin = int.MaxValue;
foreach (var index in tabIndexes.Keys)
{
if (index > tabIndex && biggerMin > index)
biggerMin = index;
if (tabIndexesMin > index)
tabIndexesMin = index;
}
tabIndex = biggerMin != int.MaxValue ? biggerMin : tabIndexesMin;
return tabIndexes[tabIndex][0];
}
}
}
}
}
Loading

0 comments on commit cceee9d

Please sign in to comment.