mirror of
https://github.com/dotnetcore/BootstrapBlazor.git
synced 2025-12-20 02:16:40 +08:00
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:
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user