Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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>
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; }
}
}
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 }}


13 changes: 13 additions & 0 deletions src/Core/Components/Drag/FluentDragContainer.razor
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
ο»Ώ@namespace Microsoft.FluentUI.AspNetCore.Components
@inherits FluentComponentBase
@typeparam TItem
@attribute [CascadingTypeParameter(nameof(TItem))]

<div id="@Id"
@attributes="@AdditionalAttributes"
class="@ClassValue"
style="@StyleValue">
<CascadingValue Value="this">
@ChildContent
</CascadingValue>
</div>
76 changes: 76 additions & 0 deletions src/Core/Components/Drag/FluentDragContainer.razor.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
// ------------------------------------------------------------------------
// This file is licensed to you under the MIT License.
// ------------------------------------------------------------------------

using Microsoft.AspNetCore.Components;

namespace Microsoft.FluentUI.AspNetCore.Components;

/// <summary />
public partial class FluentDragContainer<TItem> : FluentComponentBase
{
/// <summary />
public FluentDragContainer(LibraryConfiguration configuration) : base(configuration) { }

/// <summary />
protected virtual string? ClassValue => DefaultClassBuilder
.Build();

/// <summary />
protected virtual string? StyleValue => DefaultStyleBuilder
.Build();

/// <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();
}
}
Loading
Loading