-
Notifications
You must be signed in to change notification settings - Fork 452
[dev-v5] Add Drag & Drop component #4211
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weβll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
MarvinKlein1508
wants to merge
7
commits into
microsoft:dev-v5
Choose a base branch
from
MarvinKlein1508:v5-drag-and-drop
base: dev-v5
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from 3 commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
174abf4
Add Drag & Drop component
MarvinKlein1508 d4b3891
Add Drag & Drop demo
MarvinKlein1508 199500a
Add tests
MarvinKlein1508 549d8c7
Code review fixes
MarvinKlein1508 8b16973
Add to ComponentBaseInitializer
MarvinKlein1508 4935e6b
Code review fixes
MarvinKlein1508 2d0393f
Merge branch 'dev-v5' into v5-drag-and-drop
dvoituron File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
18 changes: 18 additions & 0 deletions
18
...ples/Demo/FluentUI.Demo.Client/Documentation/Components/Drag/Examples/DragDropBasic.razor
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
ο»Ώ<FluentDragContainer TItem="string" | ||
OnDragEnd="@(e => Console.WriteLine($"{e.Source.Id} drag ended"))" | ||
OnDragEnter="@(e => Console.WriteLine($"{e.Source.Id} is entered in {e.Target.Id}"))" | ||
OnDragLeave="@(e => Console.WriteLine($"{e.Source.Id} has left {e.Target.Id}"))" | ||
OnDropEnd="@(e => Console.WriteLine($"{e.Source.Id} dropped in {e.Target.Id}"))"> | ||
<FluentStack> | ||
<FluentDropZone Id="Item1" Draggable="true" Droppable="true"> | ||
<div style="width: 50px; height: 50px; background-color: pink;"> | ||
Item 1 | ||
</div> | ||
</FluentDropZone> | ||
<FluentDropZone Id="Item2" Draggable="true" Droppable="true"> | ||
<div style="width: 50px; height: 50px; background-color: lightgreen;"> | ||
Item 2 | ||
</div> | ||
</FluentDropZone> | ||
</FluentStack> | ||
</FluentDragContainer> |
242 changes: 242 additions & 0 deletions
242
...les/Demo/FluentUI.Demo.Client/Documentation/Components/Drag/Examples/DragDropNested.razor
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,242 @@ | ||
ο»Ώ<style> | ||
.form-editor { | ||
color: #000 !important; /* Ignore themes for this demo */ | ||
} | ||
|
||
.form-row { | ||
border: 1px dashed red; | ||
border-radius: .5em; | ||
background-color: #fff; | ||
padding: 1em; | ||
display: flex; | ||
gap: 1em; | ||
} | ||
|
||
.form-column { | ||
flex: 1; | ||
border: 2px dashed orange; | ||
border-radius: .5em; | ||
min-height: 5em; | ||
padding: .5rem; | ||
} | ||
|
||
.empty-zone { | ||
width: 100%; | ||
min-height: 50px; | ||
background-color: yellow; | ||
height: 100%; | ||
} | ||
|
||
.form-element { | ||
border: 1px solid green; | ||
border-radius: .5em; | ||
padding: 1rem; | ||
} | ||
|
||
.form-editor-flex { | ||
display: flex; | ||
flex-direction: column; | ||
gap: .5rem; | ||
} | ||
</style> | ||
<div class="form-editor"> | ||
<FluentDragContainer TItem="FormRow" OnDropEnd="OnRowDropEnd"> | ||
<FluentDragContainer TItem="FormColumn" OnDropEnd="OnColumnDropEnd"> | ||
<FluentDragContainer TItem="FormElement" OnDropEnd="OnDropElement" Class="form-editor-flex"> | ||
@foreach (var row in _testForm.Rows) | ||
{ | ||
<FluentDropZone StopPropagation="true" Item="row" Class="form-row" Draggable="true" Droppable="true"> | ||
<span>Row: @row.RowId</span> | ||
@if (row.Columns.Count == 0) | ||
{ | ||
<FluentDropZone StopPropagation="true" Data="@row" TItem="FormColumn" Class="empty-zone" Draggable="false" Droppable="true" /> | ||
} | ||
else | ||
{ | ||
@foreach (var column in row.Columns) | ||
{ | ||
<FluentDropZone StopPropagation="true" Item="column" Data="@row" Class="form-column form-editor-flex" Draggable="true" Droppable="true"> | ||
<span>Column: @column.ColumnId</span> | ||
@if (column.Elements.Count == 0) | ||
{ | ||
<FluentDropZone StopPropagation="true" Data="@column" TItem="FormElement" Class="empty-zone" Draggable="false" Droppable="true" /> | ||
} | ||
else | ||
{ | ||
@foreach (var element in column.Elements) | ||
{ | ||
<FluentDropZone StopPropagation="true" Item="element" Data="@column" Class="form-element" Draggable="true" Droppable="true"> | ||
<span>Element: @element.ElementId</span> | ||
</FluentDropZone> | ||
} | ||
} | ||
</FluentDropZone> | ||
} | ||
} | ||
</FluentDropZone> | ||
} | ||
</FluentDragContainer> | ||
</FluentDragContainer> | ||
</FluentDragContainer> | ||
</div> | ||
|
||
@code { | ||
private Form _testForm; | ||
|
||
public DragDropNested() | ||
{ | ||
_testForm = new Form(); | ||
|
||
var rows = Enumerable.Range(1, 6) | ||
.Select(id => new FormRow { RowId = id }) | ||
.ToList(); | ||
|
||
var columns = Enumerable.Range(1, 9) | ||
.Select(id => new FormColumn { ColumnId = id }) | ||
.ToList(); | ||
|
||
var elementMap = new Dictionary<int, int> | ||
{ | ||
{ 1, 2 }, | ||
{ 2, 1 }, | ||
{ 3, 3 }, | ||
{ 4, 0 }, | ||
{ 5, 2 }, | ||
{ 6, 1 }, | ||
{ 7, 0 }, | ||
{ 8, 1 }, | ||
{ 9, 0 } | ||
}; | ||
|
||
int elementIdCounter = 1; | ||
foreach (var column in columns) | ||
{ | ||
if (elementMap.TryGetValue(column.ColumnId, out int count)) | ||
{ | ||
for (int i = 0; i < count; i++) | ||
{ | ||
column.Elements.Add(new FormElement | ||
{ | ||
ElementId = elementIdCounter++ | ||
}); | ||
} | ||
} | ||
} | ||
|
||
rows[0].Columns.AddRange(new[] { columns[0], columns[1] }); | ||
rows[1].Columns.Add(columns[2]); | ||
rows[2].Columns.Add(columns[3]); | ||
rows[3].Columns.AddRange(new[] { columns[4], columns[5] }); | ||
rows[4].Columns.AddRange(new[] { columns[6], columns[7] }); | ||
rows[5].Columns.Add(columns[8]); | ||
|
||
_testForm.Rows.AddRange(rows); | ||
} | ||
|
||
private void OnRowDropEnd(FluentDragEventArgs<FormRow> e) | ||
{ | ||
var target = e.Target.Item; | ||
var source = e.Source.Item; | ||
|
||
int targetIndex = _testForm.Rows.IndexOf(target); | ||
|
||
_testForm.Rows.Remove(source); | ||
_testForm.Rows.Insert(targetIndex, source); | ||
} | ||
|
||
private void OnColumnDropEnd(FluentDragEventArgs<FormColumn> e) | ||
{ | ||
var sourceRow = e.Source.Data as FormRow; | ||
var targetRow = e.Target.Data as FormRow; | ||
|
||
if (sourceRow is null || targetRow is null) | ||
{ | ||
return; | ||
} | ||
|
||
var target = e.Target.Item; | ||
var source = e.Source.Item; | ||
int targetIndex = targetRow.Columns.IndexOf(target); | ||
|
||
if (sourceRow == targetRow) | ||
{ | ||
sourceRow.Columns.Remove(source); | ||
sourceRow.Columns.Insert(targetIndex, source); | ||
} | ||
else | ||
{ | ||
sourceRow.Columns.Remove(source); | ||
if (targetIndex != -1) | ||
{ | ||
targetRow.Columns.Insert(targetIndex, source); | ||
} | ||
else | ||
{ | ||
targetRow.Columns.Add(source); | ||
} | ||
} | ||
|
||
StateHasChanged(); | ||
} | ||
|
||
private void OnDropElement(FluentDragEventArgs<FormElement> e) | ||
{ | ||
var sourceColumn = e.Source.Data as FormColumn; | ||
var targetColumn = e.Target.Data as FormColumn; | ||
|
||
if (sourceColumn is null || targetColumn is null) | ||
{ | ||
return; | ||
} | ||
|
||
var source = e.Source.Item; | ||
var target = e.Target.Item; | ||
|
||
|
||
int targetIndex = targetColumn.Elements.IndexOf(target); | ||
|
||
|
||
if (sourceColumn == targetColumn) | ||
{ | ||
sourceColumn.Elements.Remove(source); | ||
sourceColumn.Elements.Insert(targetIndex, source); | ||
} | ||
else | ||
{ | ||
sourceColumn.Elements.Remove(source); | ||
if (targetIndex != -1) | ||
{ | ||
targetColumn.Elements.Insert(targetIndex, source); | ||
} | ||
else | ||
{ | ||
targetColumn.Elements.Add(source); | ||
} | ||
} | ||
|
||
StateHasChanged(); | ||
} | ||
|
||
public class Form | ||
{ | ||
public int FormId { get; set; } | ||
public List<FormRow> Rows { get; set; } = []; | ||
} | ||
|
||
public class FormRow | ||
{ | ||
public int RowId { get; set; } | ||
public List<FormColumn> Columns { get; set; } = []; | ||
} | ||
|
||
public class FormColumn | ||
{ | ||
public int ColumnId { get; set; } | ||
public List<FormElement> Elements { get; set; } = []; | ||
} | ||
|
||
public class FormElement | ||
{ | ||
public int ElementId { get; set; } | ||
} | ||
} |
33 changes: 33 additions & 0 deletions
33
examples/Demo/FluentUI.Demo.Client/Documentation/Components/Drag/FluentDragDrop.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
--- | ||
title: Drag and Drop | ||
route: /Drag | ||
--- | ||
|
||
# Drag and Drop | ||
|
||
A web component implementation of a <a href="https://developer.mozilla.org/en-US/docs/Web/API/HTML_Drag_and_Drop_API" target="_blank">HTML Drag and Drop API</a>. | ||
|
||
The user may select draggable elements with a mouse, drag those elements to a droppable element, and drop them by releasing the mouse button. A translucent representation of the draggable elements follows the pointer during the drag operation. | ||
|
||
## Basic example | ||
|
||
{{ DragDropBasic }} | ||
|
||
## Nested drag & drop | ||
This example demonstrates how to nest multiple `FluentDragContainer` components to enable drag-and-drop interactions across hierarchical structures such as rows, columns, and elements. | ||
|
||
The key to making this multi-level drag-and-drop system work is the use of the `StopPropagation` property. By enabling it where appropriate, each nested `FluentDragContainer` can handle drag events independently-preventing unintended interference from parent containers. | ||
|
||
Each level supports independent drag behavior: | ||
|
||
* **Rows** can be reordered vertically. | ||
* **Columns** can be moved within the same row or between different rows. | ||
* **Elements** can be rearranged inside a column or moved across columns. | ||
|
||
The <span style="background-color: yellow; padding: 0 0.2em; color: #000;">yellow area</span> indicates an empty drop zone inside a row. It accepts columns when a row does not contain any yet. Similarly, empty columns behave as drop zones for elements. | ||
|
||
This structure allows fully flexible layout editing with deep nesting and drag-and-drop handling at every level. | ||
|
||
{{ DragDropNested }} | ||
|
||
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
@namespace Microsoft.FluentUI.AspNetCore.Components | ||
@inherits FluentComponentBase | ||
|
||
@typeparam TItem | ||
@attribute [CascadingTypeParameter(nameof(TItem))] | ||
<div id=@Id | ||
class=@ClassValue | ||
style=@StyleValue> | ||
<CascadingValue Value="this"> | ||
@ChildContent | ||
</CascadingValue> | ||
</div> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,75 @@ | ||
// ------------------------------------------------------------------------ | ||
// This file is licensed to you under the MIT License. | ||
// ------------------------------------------------------------------------ | ||
|
||
using Microsoft.AspNetCore.Components; | ||
using Microsoft.FluentUI.AspNetCore.Components.Utilities; | ||
|
||
namespace Microsoft.FluentUI.AspNetCore.Components; | ||
|
||
/// <summary /> | ||
public partial class FluentDragContainer<TItem> : FluentComponentBase | ||
{ | ||
/// <summary /> | ||
public FluentDragContainer(LibraryConfiguration configuration) : base(configuration) { } | ||
|
||
/// <summary /> | ||
protected virtual string? ClassValue => new CssBuilder(Class).Build(); | ||
|
||
/// <summary /> | ||
protected virtual string? StyleValue => new StyleBuilder(Style).Build(); | ||
MarvinKlein1508 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
||
/// <summary> | ||
/// Gets or sets the content to be rendered inside the component. | ||
/// </summary> | ||
[Parameter] | ||
public RenderFragment? ChildContent { get; set; } | ||
|
||
/// <summary> | ||
/// This event is fired when the user starts dragging an element. | ||
/// </summary> | ||
[Parameter] | ||
public Action<FluentDragEventArgs<TItem>>? OnDragStart { get; set; } | ||
|
||
/// <summary> | ||
/// This event is fired when the drag operation ends (such as releasing a mouse button or hitting the Esc key). | ||
/// </summary> | ||
[Parameter] | ||
public Action<FluentDragEventArgs<TItem>>? OnDragEnd { get; set; } | ||
|
||
/// <summary> | ||
/// This event is fired when a dragged element enters a valid drop target. | ||
/// </summary> | ||
[Parameter] | ||
public Action<FluentDragEventArgs<TItem>>? OnDragEnter { get; set; } | ||
|
||
/// <summary> | ||
/// This event is fired when an element is being dragged over a valid drop target. | ||
/// </summary> | ||
[Parameter] | ||
public Action<FluentDragEventArgs<TItem>>? OnDragOver { get; set; } | ||
|
||
/// <summary> | ||
/// This event is fired when a dragged element leaves a valid drop target. | ||
/// </summary> | ||
[Parameter] | ||
public Action<FluentDragEventArgs<TItem>>? OnDragLeave { get; set; } | ||
|
||
/// <summary> | ||
/// This event is fired when an element is dropped on a valid drop target. | ||
/// </summary> | ||
[Parameter] | ||
public Action<FluentDragEventArgs<TItem>>? OnDropEnd { get; set; } | ||
|
||
/// <summary> | ||
/// property to keep the zone currently dragged. | ||
/// </summary> | ||
internal FluentDropZone<TItem>? StartedZone { get; private set; } | ||
|
||
/// <summary /> | ||
internal void SetStartedZone(FluentDropZone<TItem>? value) | ||
{ | ||
StartedZone = value; | ||
StateHasChanged(); | ||
} | ||
} |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.