feat(ContextMenu): support mobile device (#1519)

* ContextMenuWIthTouchEvent

* refactor: 重构代码

* revert: 撤销对 Zone 的更改

* refactor: Trigger 支持移动端

* refactor: 移除取消 Token 代码

* feat(Table): 支持移动端

* feat(Table): 支持移动端右键菜单

* feat(TreeView): 支持右键菜单

* chore: 更新项目引用

* fix: 修复 CardView 模式脚本报错问题

* feat: 移动端支持右键菜单

* test: 更新单元测试

* test: 增加 Camera 单元测试

* refactor: 删除 Token

* test: 增加 Trigger 单元测试

* test: 增加 Table 单元测试

* test: 增加 TreeView 单元测试

* test: 增加重复 Touch 单元测试

---------

Co-authored-by: Argo-AscioTech <argo@live.ca>
This commit is contained in:
Alex chow
2023-07-13 10:46:25 +08:00
committed by GitHub
parent b2de5537cb
commit bf35ad395f
8 changed files with 192 additions and 22 deletions

View File

@@ -45,11 +45,13 @@ public class ContextMenuTrigger : BootstrapComponentBase
protected override void BuildRenderTree(RenderTreeBuilder builder)
{
builder.OpenElement(0, WrapperTag);
builder.AddMultipleAttributes(1, AdditionalAttributes);
builder.AddAttribute(2, "class", ClassString);
builder.AddAttribute(3, "oncontextmenu", EventCallback.Factory.Create<MouseEventArgs>(this, OnContextMenu));
builder.AddEventPreventDefaultAttribute(4, "oncontextmenu", true);
builder.AddContent(5, ChildContent);
builder.AddMultipleAttributes(10, AdditionalAttributes);
builder.AddAttribute(20, "class", ClassString);
builder.AddAttribute(30, "oncontextmenu", EventCallback.Factory.Create<MouseEventArgs>(this, OnContextMenu));
builder.AddAttribute(35, "ontouchstart", EventCallback.Factory.Create<TouchEventArgs>(this, OnTouchStart));
builder.AddAttribute(36, "ontouchend", EventCallback.Factory.Create<TouchEventArgs>(this, OnTouchEnd));
builder.AddEventPreventDefaultAttribute(40, "oncontextmenu", true);
builder.AddContent(50, ChildContent);
builder.CloseElement();
}
@@ -58,4 +60,47 @@ public class ContextMenuTrigger : BootstrapComponentBase
/// </summary>
/// <returns></returns>
public Task OnContextMenu(MouseEventArgs args) => ContextMenuZone.OnContextMenu(args, ContextItem);
/// <summary>
/// 是否触摸
/// </summary>
private bool TouchStart { get; set; }
/// <summary>
/// 触摸定时器工作指示
/// </summary>
private bool IsBusy { get; set; }
private async Task OnTouchStart(TouchEventArgs e)
{
if (!IsBusy)
{
IsBusy = true;
TouchStart = true;
// 延时保持 TouchStart 状态
await Task.Delay(200);
if (TouchStart)
{
var args = new MouseEventArgs()
{
ClientX = e.Touches[0].ClientX,
ClientY = e.Touches[0].ClientY,
ScreenX = e.Touches[0].ScreenX,
ScreenY = e.Touches[0].ScreenY,
};
// 弹出关联菜单
await OnContextMenu(args);
//延时防止重复激活菜单功能
await Task.Delay(200);
}
IsBusy = false;
}
}
private void OnTouchEnd()
{
TouchStart = false;
}
}

View File

@@ -207,7 +207,7 @@
{
OnBeforeRenderRow?.Invoke(item);
<DynamicElement class="@GetRowClassString(item, "table-row")"
TriggerContextMenu="ContextMenuZone != null" OnContextMenu="e => OnContextMenu(e, item)"
TriggerContextMenu="ContextMenuZone != null" OnContextMenu="e => OnContextMenu(e, item)" @ontouchstart="e => OnTouchStart(e, item)" @ontouchend="OnTouchEnd"
TriggerClick="@(ClickToSelect || OnClickRowCallback != null)" OnClick="() => ClickRow(item)">
@if (IsMultipleSelect)
{
@@ -599,7 +599,7 @@
RenderFragment<TItem> RenderRow => item =>
@<DynamicElement TagName="tr" class="@GetRowClassString(item)"
TriggerContextMenu="ContextMenuZone != null" OnContextMenu="e => OnContextMenu(e, item)"
TriggerContextMenu="ContextMenuZone != null" OnContextMenu="e => OnContextMenu(e, item)" @ontouchstart="e => OnTouchStart(e, item)" @ontouchend="OnTouchEnd"
TriggerClick="@(ClickToSelect || OnClickRowCallback != null)" OnClick="() => ClickRow(item)"
TriggerDoubleClick="@(DoubleClickToEdit || OnDoubleClickRowCallback != null)" OnDoubleClick="() => DoubleClickRow(item)">
@if (ShowDetails())

View File

@@ -49,7 +49,8 @@ public partial class Table<TItem> : ITable, IModelEqualityComparer<TItem> where
/// <summary>
/// 获得 wrapper 样式表集合
/// </summary>
protected string? WrapperClassName => CssBuilder.Default("table-shim")
protected string? WrapperClassName => CssBuilder.Default()
.AddClass("table-shim", ActiveRenderMode == TableRenderMode.Table)
.AddClass("table-wrapper", IsBordered)
.AddClass("is-clickable", ClickToSelect || DoubleClickToEdit || OnClickRowCallback != null || OnDoubleClickRowCallback != null)
.AddClass("table-scroll", !IsFixedHeader || FixedColumn)
@@ -1219,6 +1220,49 @@ public partial class Table<TItem> : ITable, IModelEqualityComparer<TItem> where
}
}
/// <summary>
/// 是否触摸
/// </summary>
private bool TouchStart { get; set; }
/// <summary>
/// 触摸定时器工作指示
/// </summary>
private bool IsBusy { get; set; }
private async Task OnTouchStart(TouchEventArgs e, TItem item)
{
if (!IsBusy && ContextMenuZone != null)
{
IsBusy = true;
TouchStart = true;
// 延时保持 TouchStart 状态
await Task.Delay(200);
if (TouchStart)
{
var args = new MouseEventArgs()
{
ClientX = e.Touches[0].ClientX,
ClientY = e.Touches[0].ClientY,
ScreenX = e.Touches[0].ScreenX,
ScreenY = e.Touches[0].ScreenY,
};
// 弹出关联菜单
await OnContextMenu(args, item);
//延时防止重复激活菜单功能
await Task.Delay(200);
}
IsBusy = false;
}
}
private void OnTouchEnd()
{
TouchStart = false;
}
/// <summary>
/// Dispose 方法
/// </summary>

View File

@@ -39,7 +39,7 @@ else
private RenderFragment<TreeViewItem<TItem>> RenderTreeItem => item =>
@<li class="@GetItemClassString(item)">
<div class="tree-content" @oncontextmenu="e => OnContextMenu(e, item)" @oncontextmenu:preventDefault="IsPreventDefault">
<div class="tree-content" @oncontextmenu="e => OnContextMenu(e, item)" @oncontextmenu:preventDefault="IsPreventDefault" @ontouchstart="e => OnTouchStart(e, item)" @ontouchend="OnTouchEnd">
<DynamicElement TagName="i" class="@GetCaretClassString(item)" TriggerClick="TriggerNodeArrow(item)" OnClick="() => OnToggleNodeAsync(item, true)"></DynamicElement>
@if (ShowCheckbox)
{

View File

@@ -451,4 +451,47 @@ public partial class TreeView<TItem> : IModelEqualityComparer<TItem>
}
private bool IsPreventDefault => ContextMenuZone != null;
/// <summary>
/// 是否触摸
/// </summary>
private bool TouchStart { get; set; }
/// <summary>
/// 触摸定时器工作指示
/// </summary>
private bool IsBusy { get; set; }
private async Task OnTouchStart(TouchEventArgs e, TreeViewItem<TItem> item)
{
if (!IsBusy && ContextMenuZone != null)
{
IsBusy = true;
TouchStart = true;
// 延时保持 TouchStart 状态
await Task.Delay(200);
if (TouchStart)
{
var args = new MouseEventArgs()
{
ClientX = e.Touches[0].ClientX,
ClientY = e.Touches[0].ClientY,
ScreenX = e.Touches[0].ScreenX,
ScreenY = e.Touches[0].ScreenY,
};
// 弹出关联菜单
await OnContextMenu(args, item);
//延时防止重复激活菜单功能
await Task.Delay(200);
}
IsBusy = false;
}
}
private void OnTouchEnd()
{
TouchStart = false;
}
}

View File

@@ -121,8 +121,13 @@ public class CameraTest : BootstrapBlazorTestBase
{
pb.Add(a => a.VideoWidth, 30);
pb.Add(a => a.VideoHeight, 20);
pb.Add(a => a.CaptureJpeg, true);
pb.Add(a => a.Quality, 0.9);
});
Assert.Equal(40, cut.Instance.VideoWidth);
Assert.Equal(30, cut.Instance.VideoHeight);
Assert.True(cut.Instance.CaptureJpeg);
Assert.Equal(0.9, cut.Instance.Quality);
}
}

View File

@@ -2,7 +2,9 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
// Website: https://www.blazor.zone or https://argozhang.github.io/
using AngleSharp.Dom;
using BootstrapBlazor.Shared;
using Microsoft.AspNetCore.Components.Web;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Localization;
@@ -11,7 +13,7 @@ namespace UnitTest.Components;
public class ContextMenuTest : BootstrapBlazorTestBase
{
[Fact]
public void ContextMenu_Ok()
public async Task ContextMenu_Ok()
{
var localizer = Context.Services.GetRequiredService<IStringLocalizer<Foo>>();
var foo = Foo.Generate(localizer);
@@ -48,7 +50,7 @@ public class ContextMenuTest : BootstrapBlazorTestBase
});
});
cut.InvokeAsync(() =>
await cut.InvokeAsync(async () =>
{
var row = cut.Find(".context-trigger");
row.ContextMenu(0, 10, 10, 10, 10, 2, 2);
@@ -64,13 +66,19 @@ public class ContextMenuTest : BootstrapBlazorTestBase
var item = menu.Find(".dropdown-item");
item.Click();
Assert.False(clicked);
// 测试 Touch 事件
TriggerTouchStart(row);
await Task.Delay(500);
row.TouchEnd();
});
}
[Theory]
[InlineData(TableRenderMode.Table)]
[InlineData(TableRenderMode.CardView)]
public void ContextMenu_Table(TableRenderMode renderMode)
public async Task ContextMenu_Table(TableRenderMode renderMode)
{
var localizer = Context.Services.GetRequiredService<IStringLocalizer<Foo>>();
var items = Foo.GenerateFoo(localizer, 2);
@@ -106,7 +114,7 @@ public class ContextMenuTest : BootstrapBlazorTestBase
});
});
cut.InvokeAsync(() =>
await cut.InvokeAsync(async () =>
{
var row = renderMode == TableRenderMode.CardView ? cut.Find(".table-row") : cut.Find("tbody tr");
row.ContextMenu(0, 10, 10, 10, 10, 2, 2);
@@ -121,11 +129,17 @@ public class ContextMenuTest : BootstrapBlazorTestBase
var item = menu.Find(".dropdown-item");
item.Click();
Assert.True(clicked);
TriggerTouchStart(row);
TriggerTouchStart(row);
await Task.Delay(500);
row.TouchEnd();
});
}
[Fact]
public void ContextMenu_TreeView()
public async Task ContextMenu_TreeView()
{
var items = new List<TreeFoo>
{
@@ -165,7 +179,7 @@ public class ContextMenuTest : BootstrapBlazorTestBase
});
});
cut.InvokeAsync(() =>
await cut.InvokeAsync(async () =>
{
var row = cut.Find(".tree-content");
row.ContextMenu(0, 10, 10, 10, 10, 2, 2);
@@ -180,6 +194,29 @@ public class ContextMenuTest : BootstrapBlazorTestBase
var item = menu.Find(".dropdown-item");
item.Click();
Assert.True(clicked);
TriggerTouchStart(row);
TriggerTouchStart(row);
await Task.Delay(500);
row.TouchEnd();
});
}
private void TriggerTouchStart(IElement row)
{
row.TouchStart(new TouchEventArgs()
{
Touches = new TouchPoint[]
{
new()
{
ClientX = 10,
ClientY = 10,
ScreenX = 10,
ScreenY = 10
}
}
});
}
}

View File

@@ -6743,15 +6743,11 @@ public class TableTest : TableTestBase
{
var table = cut.FindComponent<Table<Foo>>();
table.Instance.ResetColumnsCallback(1, 0);
});
var columns = cut.FindAll("th");
Assert.Contains("地址", columns[0].InnerHtml);
Assert.Contains("姓名", columns[1].InnerHtml);
var columns = cut.FindAll("th");
Assert.Contains("地址", columns[0].InnerHtml);
Assert.Contains("姓名", columns[1].InnerHtml);
cut.InvokeAsync(() =>
{
var table = cut.FindComponent<Table<Foo>>();
table.Instance.ResetColumnsCallback(2, 3);
});
}