Skip to content

Commit ac8ea57

Browse files
authored
refactor(TreeView): redesign TreeView dom structure (#4696)
* refactor: 更新 Checkbox 脚本 * refactor: 增加 HasChildren 检查 * chore: bump version 9.0.1-beta04 * style: 调整样式 * refactor: 统一逻辑 * test: 更新单元测试 * refactor: 更新单元测试 * test: 更新单元测试
1 parent 4673c39 commit ac8ea57

File tree

9 files changed

+166
-179
lines changed

9 files changed

+166
-179
lines changed

src/BootstrapBlazor/BootstrapBlazor.csproj

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<Project Sdk="Microsoft.NET.Sdk.Razor">
22

33
<PropertyGroup>
4-
<Version>9.0.1-beta03</Version>
4+
<Version>9.0.1-beta04</Version>
55
</PropertyGroup>
66

77
<ItemGroup>

src/BootstrapBlazor/Components/Checkbox/Checkbox.razor.js

+3-3
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ export function init(id, invoke, method) {
88
}
99

1010
EventHandler.on(el, 'click', async e => {
11+
e.preventDefault();
1112
const stopPropagation = el.getAttribute("data-bb-stop-propagation");
1213
if (stopPropagation === "true") {
1314
e.stopPropagation();
@@ -26,10 +27,9 @@ export function init(id, invoke, method) {
2627
el.parentElement.classList.add('is-checked');
2728
}
2829
}
30+
2931
const result = await invoke.invokeMethodAsync(method, val);
30-
if (result === false) {
31-
e.preventDefault();
32-
}
32+
return result;
3333
});
3434
}
3535

src/BootstrapBlazor/Components/TreeView/TreeView.razor

+6-23
Original file line numberDiff line numberDiff line change
@@ -50,37 +50,20 @@ else
5050
}
5151
else
5252
{
53-
<ul class="tree-root scroll" tabindex="0">
54-
@foreach (var item in Items)
53+
<div class="tree-root scroll" tabindex="0">
54+
@foreach (var item in Rows)
5555
{
56-
@RenderTreeItem(item)
56+
@RenderTreeRow(item)
5757
}
58-
</ul>
58+
</div>
5959
}
6060
</div>
6161
}
6262

6363
@code {
64-
private RenderFragment<TreeViewItem<TItem>> RenderTreeNode => item =>
65-
@<ul class="@GetTreeClassString(item)">
66-
@foreach (var node in item.Items)
67-
{
68-
@RenderTreeItem(node)
69-
}
70-
</ul>;
71-
72-
private RenderFragment<TreeViewItem<TItem>> RenderTreeItem => item =>
73-
@<li class="@GetItemClassString(item)">
74-
@RenderTreeRow(item)
75-
@if (item.Items.Any())
76-
{
77-
@RenderTreeNode(item)
78-
}
79-
</li>;
80-
8164
private RenderFragment<TreeViewItem<TItem>> RenderTreeRow => item =>
82-
@<div @key="item" class="tree-content" data-bb-tree-view-index="@Rows.IndexOf(item)" @oncontextmenu="e => OnContextMenu(e, item)" @oncontextmenu:preventDefault="IsPreventDefault" @ontouchstart="e => OnTouchStart(e, item)" @ontouchend="OnTouchEnd" style="@GetTreeRowStyle(item)">
83-
<DynamicElement TagName="i" class="@GetCaretClassString(item)" TriggerClick="TriggerNodeArrow(item)" OnClick="() => OnToggleNodeAsync(item, true)"></DynamicElement>
65+
@<div @key="item" class="@GetContentClassString(item)" data-bb-tree-view-index="@Rows.IndexOf(item)" @oncontextmenu="e => OnContextMenu(e, item)" @oncontextmenu:preventDefault="IsPreventDefault" @ontouchstart="e => OnTouchStart(e, item)" @ontouchend="OnTouchEnd" style="@GetTreeRowStyle(item)">
66+
<DynamicElement TagName="i" class="@GetCaretClassString(item)" TriggerClick="CanTriggerClickNode(item)" OnClick="() => OnToggleNodeAsync(item, true)"></DynamicElement>
8467
<i class="@NodeLoadingClassString"></i>
8568
@if (ShowCheckbox)
8669
{

src/BootstrapBlazor/Components/TreeView/TreeView.razor.cs

+25-31
Original file line numberDiff line numberDiff line change
@@ -50,37 +50,36 @@ public partial class TreeView<TItem> : IModelEqualityComparer<TItem>
5050
.AddClass("visible", item.HasChildren || item.Items.Count > 0)
5151
.AddClass(NodeIcon, !item.IsExpand)
5252
.AddClass(ExpandNodeIcon, item.IsExpand)
53-
.AddClass("disabled", !CanExpandWhenDisabled && GetItemDisabledState(item))
53+
.AddClass("disabled", IsDisabled || (!CanExpandWhenDisabled && item.IsDisabled))
5454
.Build();
5555

5656
private string? NodeLoadingClassString => CssBuilder.Default("node-icon node-loading")
5757
.AddClass(LoadingIcon)
5858
.Build();
5959

60-
/// <summary>
61-
/// 获得/设置 当前行样式
62-
/// </summary>
63-
/// <param name="item"></param>
64-
/// <returns></returns>
65-
private string? GetItemClassString(TreeViewItem<TItem> item) => CssBuilder.Default("tree-item")
60+
private string? GetContentClassString(TreeViewItem<TItem> item) => CssBuilder.Default("tree-content")
6661
.AddClass("active", ActiveItem == item)
67-
.AddClass("disabled", !CanExpandWhenDisabled && GetItemDisabledState(item))
68-
.Build();
69-
70-
/// <summary>
71-
/// 获得/设置 Tree 样式
72-
/// </summary>
73-
/// <param name="item"></param>
74-
/// <returns></returns>
75-
private static string? GetTreeClassString(TreeViewItem<TItem> item) => CssBuilder.Default("tree-ul")
76-
.AddClass("show", item.IsExpand)
7762
.Build();
7863

7964
private string? GetNodeClassString(TreeViewItem<TItem> item) => CssBuilder.Default("tree-node")
8065
.AddClass("disabled", GetItemDisabledState(item))
8166
.Build();
8267

83-
private bool TriggerNodeArrow(TreeViewItem<TItem> item) => (CanExpandWhenDisabled || !GetItemDisabledState(item)) && (item.HasChildren || item.Items.Count > 0);
68+
private bool CanTriggerClickNode(TreeViewItem<TItem> item)
69+
{
70+
// 返回 false 时禁止触发 OnClick
71+
if (IsDisabled)
72+
{
73+
return false;
74+
}
75+
76+
if (CanExpandWhenDisabled)
77+
{
78+
return true;
79+
}
80+
81+
return !item.IsDisabled;
82+
}
8483

8584
private bool TriggerNodeLabel(TreeViewItem<TItem> item) => !GetItemDisabledState(item);
8685

@@ -592,7 +591,7 @@ private async Task<IEnumerable<IExpandableNode<TItem>>> GetChildrenRowAsync(Tree
592591
private async Task OnClick(TreeViewItem<TItem> item)
593592
{
594593
ActiveItem = item;
595-
if (ClickToggleNode && TriggerNodeArrow(item))
594+
if (ClickToggleNode && CanTriggerClickNode(item))
596595
{
597596
await OnToggleNodeAsync(item);
598597
}
@@ -874,20 +873,15 @@ private List<TreeViewItem<TItem>> GetTreeRows(List<TreeViewItem<TItem>> items)
874873
}
875874
}
876875

877-
private string? GetTreeRowStyle(TreeViewItem<TItem> item)
876+
private static string? GetTreeRowStyle(TreeViewItem<TItem> item)
878877
{
879-
string? style = null;
880-
if (IsVirtualize)
878+
var level = 0;
879+
var parent = item.Parent;
880+
while (parent != null)
881881
{
882-
var level = 0;
883-
var parent = item.Parent;
884-
while (parent != null)
885-
{
886-
level++;
887-
parent = parent.Parent;
888-
}
889-
style = $"--bb-tree-view-level: {level};";
882+
level++;
883+
parent = parent.Parent;
890884
}
891-
return style;
885+
return $"--bb-tree-view-level: {level};";
892886
}
893887
}
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
.tree-view {
22
--bb-tree-padding: #{$bb-tree-padding};
33
--bb-tree-margin: #{$bb-tree-margin};
4-
--bb-tree-ul-padding-left: #{$bb-tree-ul-padding-left};
4+
--bb-tree-padding-left: #{$bb-tree-padding-left};
55
--bb-tree-item-margin: #{$bb-tree-item-margin};
66
--bb-tree-icon-width: #{$bb-tree-icon-width};
77
--bb-tree-check-margin: #{$bb-tree-check-margin};
@@ -18,36 +18,21 @@
1818
.tree-search {
1919
margin-block-end: .5rem;
2020
}
21-
}
22-
23-
.tree-view .tree-root {
24-
padding: var(--bb-tree-padding);
25-
margin: var(--bb-tree-margin);
26-
}
27-
28-
.tree-view.is-fixed-search .tree-root {
29-
height: calc(100% - var(--bb-tree-search-height));
30-
}
31-
32-
.tree-view .tree-ul {
33-
padding: 0 0 0 var(--bb-tree-ul-padding-left);
34-
display: none;
35-
}
3621

37-
.tree-view .tree-ul.show {
38-
display: block;
39-
}
22+
&.is-fixed-search .tree-root {
23+
height: calc(100% - var(--bb-tree-search-height));
24+
}
4025

41-
.tree-view .tree-item {
42-
list-style: none;
43-
margin: var(--bb-tree-item-margin);
44-
}
26+
.tree-root {
27+
padding: var(--bb-tree-padding);
28+
margin: var(--bb-tree-margin);
29+
}
4530

46-
.tree-view {
4731
.tree-content {
4832
position: relative;
4933
display: flex;
5034
align-items: center;
35+
margin-inline-start: calc(var(--bb-tree-padding-left) * var(--bb-tree-view-level, 0));
5136
cursor: pointer;
5237

5338
.node-icon {
@@ -84,49 +69,50 @@
8469
display: flex;
8570
}
8671
}
87-
}
8872

89-
.is-virtual {
90-
.tree-content {
91-
margin-inline-start: calc(var(--bb-tree-ul-padding-left) * var(--bb-tree-view-level, 0));
73+
&:after {
74+
content: "";
75+
position: absolute;
76+
left: 0;
77+
right: 0;
78+
top: 0;
79+
bottom: 0;
80+
pointer-events: none;
81+
margin-inline-start: calc(var(--bb-tree-padding-left) * var(--bb-tree-view-level, 0) * -1);
9282
}
93-
}
94-
}
9583

96-
.tree-view .tree-node {
97-
display: inline-flex;
98-
align-items: center;
99-
padding: var(--bb-tree-node-padding);
100-
border-radius: var(--bs-border-radius);
101-
flex: 1;
102-
103-
&.disabled {
104-
opacity: var(--bb-tree-disabled-opacity);
105-
}
84+
&:not(.disabled).active:after {
85+
color: var(--bb-tree-item-active-color);
86+
background-color: var(--bb-tree-item-active-bg);
87+
}
10688

107-
&:hover {
108-
background-color: var(--bb-tree-item-hover-bg);
89+
&:not(.disabled):hover:after {
90+
background-color: var(--bb-tree-item-hover-bg);
91+
}
10992
}
11093

111-
.tree-node-text {
112-
white-space: nowrap;
113-
}
114-
}
94+
.tree-node {
95+
display: inline-flex;
96+
align-items: center;
97+
padding: var(--bb-tree-node-padding);
98+
flex: 1;
11599

116-
.tree-view .tree-node .tree-icon {
117-
margin-inline-end: var(--bb-tree-icon-margin-right);
118-
}
100+
.form-check {
101+
margin: var(--bb-tree-check-margin);
102+
}
119103

120-
.tree-view .form-check {
121-
margin: var(--bb-tree-check-margin);
122-
}
104+
.tree-icon {
105+
width: var(--bb-tree-icon-width);
106+
text-align: center;
107+
margin-inline-end: var(--bb-tree-icon-margin-right);
108+
}
123109

124-
.tree-view .tree-icon {
125-
width: var(--bb-tree-icon-width);
126-
text-align: center;
127-
}
110+
.tree-node-text {
111+
white-space: nowrap;
112+
}
128113

129-
.tree-view .tree-item:not(.disabled).active > .tree-content > .tree-node {
130-
color: var(--bb-tree-item-active-color);
131-
background-color: var(--bb-tree-item-active-bg);
114+
&.disabled {
115+
opacity: var(--bb-tree-disabled-opacity);
116+
}
117+
}
132118
}

src/BootstrapBlazor/Misc/ExpandableNodeCache.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ public virtual async Task ToggleNodeAsync(TNode node, Func<TNode, Task<IEnumerab
5454
CollapsedNodeCache.Remove(node.Value);
5555

5656
// 无子项时通过回调方法延时加载
57-
if (!node.Items.Any())
57+
if (node.HasChildren && !node.Items.Any())
5858
{
5959
var items = await callback(node);
6060
node.Items = items.ToList();

src/BootstrapBlazor/wwwroot/scss/theme/bootstrapblazor.scss

+1-1
Original file line numberDiff line numberDiff line change
@@ -665,7 +665,7 @@ $bb-transfer-filter-margin: .5rem;
665665
// TreeView
666666
$bb-tree-padding: 0;
667667
$bb-tree-margin: 0;
668-
$bb-tree-ul-padding-left: 20px;
668+
$bb-tree-padding-left: 20px;
669669
$bb-tree-item-margin: 1px 0;
670670
$bb-tree-icon-width: 22px;
671671
$bb-tree-check-margin: 0 4px;

test/UnitTest/Components/SelectTreeTest.cs

+8-7
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ public void PlaceHolder_Ok()
109109
}
110110

111111
[Fact]
112-
public void ItemChanged_Ok()
112+
public async Task ItemChanged_Ok()
113113
{
114114
var changed = 0;
115115
var cut = Context.RenderComponent<SelectTree<string>>(builder =>
@@ -123,13 +123,14 @@ public void ItemChanged_Ok()
123123
});
124124
Assert.Equal(1, changed);
125125

126-
// 选择第一个候选项
127-
var node = cut.Find(".tree-node");
128-
cut.InvokeAsync(() => node.Click());
129-
Assert.NotEqual(2, changed);
126+
// 展开第一个候选项
127+
var icon = cut.Find(".node-icon");
128+
await cut.InvokeAsync(() => icon.Click());
129+
Assert.Equal(1, changed);
130130

131-
node = cut.FindAll(".tree-node").Skip(1).Take(1).First();
132-
cut.InvokeAsync(() => node.Click());
131+
// 点击第二个节点
132+
var nodes = cut.FindAll(".tree-node");
133+
await cut.InvokeAsync(() => nodes[1].Click());
133134
Assert.Equal(2, changed);
134135
}
135136

0 commit comments

Comments
 (0)