Skip to content
Merged
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
1 change: 1 addition & 0 deletions FluentKit.slnx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<Solution>
<Project Path="samples/Fluentkit.Sample.Maui/Fluentkit.Sample.Maui.csproj" />
<Project Path="samples/FluentKit.Sample.Shared/FluentKit.Sample.Shared.csproj" />
<Project Path="samples/FluentKit.Sample.Server/FluentKit.Sample.Server.csproj" />
<Project Path="samples/FluentKit.Sample.Wasm/FluentKit.Sample.Wasm.csproj" />
<Project Path="src/FluentKit/FluentKit.csproj" />
<Project Path="src/FluentKit.IconGenerator/FluentKit.IconGenerator.csproj" />
Expand Down
11 changes: 10 additions & 1 deletion docs/integration/overlays.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,16 @@

Register `IOverlayService` and render one `FluentOverlayHost` inside the root
`ThemeProvider`. The host is the portal target for `FluentTooltip`, `FluentFlyout`, menus,
`FluentContentDialog`, and `FluentTeachingTip`.
`FluentComboBox`, `FluentAutoSuggestBox`, `FluentContentDialog`, and `FluentTeachingTip`.

ComboBox and AutoSuggestBox dropdowns are portalled so they can escape native labels, clipping
ancestors, and backdrop-filter boundaries. They use the shared `OverlaySurface` with
`OverlayContentLayout.EdgeToEdge`; regular flyouts keep the default padded surface. Applications
that create overlays directly can pass `OverlaySurfaceOptions` to select the content layout or
entrance origin, and `OverlayPositioningOptions` to align an anchored surface at the anchor start
with a main-axis offset. `OverlayAnimationOptions` can override entrance or exit duration and
easing; entrances default to WinUI's fast-out, slow-in motion, so they begin at maximum velocity
and gradually decelerate to rest.

Keep an overlay host mounted for the lifetime of the application. Rendering a second host
or placing it inside a transient page can make an overlay disappear when navigation occurs.
Expand Down
200 changes: 186 additions & 14 deletions docs/reference/api.json

Large diffs are not rendered by default.

41 changes: 24 additions & 17 deletions docs/reference/api.md

Large diffs are not rendered by default.

27 changes: 27 additions & 0 deletions samples/FluentKit.Sample.Server/Components/App.razor
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>FluentKit Gallery — Interactive Server</title>
<base href="/" />
<link rel="stylesheet" href="_content/FluentKit/Tokens/tokens.css" />
<link rel="stylesheet" href="_content/FluentKit/Icons/FluentSystemIcons-Regular.css" />
<link rel="stylesheet" href="FluentKit.Sample.Server.styles.css" />
<link rel="stylesheet" href="_content/FluentKit.Sample.Shared/gallery.css" />
<HeadOutlet @rendermode="InteractiveServer" />
</head>
<body>
<Routes @rendermode="InteractiveServer" />
<script>
window.galleryHighlight = function (elementId) { var el = document.getElementById(elementId); if (el && window.Prism) window.Prism.highlightElement(el); };
window.galleryHighlightAll = function () { if (window.Prism) window.Prism.highlightAll(); };
window.liveHighlight = function (elementId, code) { var el = document.getElementById(elementId); if (el) el.textContent = code; };
window.liveHighlightSyncScroll = function () { };
window.galleryStorageGet = function (key) { try { return localStorage.getItem(key); } catch { return null; } };
window.galleryStorageSet = function (key, value) { try { localStorage.setItem(key, value); return true; } catch { return false; } };
window.galleryStorageRemove = function (key) { try { localStorage.removeItem(key); } catch { } };
</script>
<script src="_framework/blazor.web.js"></script>
</body>
</html>
9 changes: 9 additions & 0 deletions samples/FluentKit.Sample.Server/Components/Routes.razor
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<Router AppAssembly="@typeof(MainLayout).Assembly">
<Found Context="routeData">
<RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
<FocusOnNavigate RouteData="@routeData" Selector="h1" />
</Found>
<NotFound>
<p>Not found.</p>
</NotFound>
</Router>
5 changes: 5 additions & 0 deletions samples/FluentKit.Sample.Server/Components/_Imports.razor
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
@using Microsoft.AspNetCore.Components.Web
@using Microsoft.AspNetCore.Components.Routing
@using static Microsoft.AspNetCore.Components.Web.RenderMode
@using FluentKit.Sample.Shared
@using FluentKit.Sample.Shared.Shared
12 changes: 12 additions & 0 deletions samples/FluentKit.Sample.Server/FluentKit.Sample.Server.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\..\src\FluentKit\FluentKit.csproj" />
<ProjectReference Include="..\FluentKit.Sample.Shared\FluentKit.Sample.Shared.csproj" />
</ItemGroup>
</Project>
30 changes: 30 additions & 0 deletions samples/FluentKit.Sample.Server/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
using FluentKit.Overlay;
using FluentKit.Sample.Server.Components;
using FluentKit.Sample.Server.Services;
using FluentKit.Sample.Shared;
using FluentKit.Sample.Shared.Services;
using FluentKit.Sample.Shared.Shared;
using FluentKit.Theming;

var builder = WebApplication.CreateBuilder(args);
// Ensure Rider/debug launches load this project's RCL static-web-asset manifest as well as
// published deployments, rather than returning empty matches for _content/* files.
builder.WebHost.UseStaticWebAssets();

builder.Services.AddRazorComponents().AddInteractiveServerComponents();
builder.Services.AddScoped<IThemeService, ThemeService>();
builder.Services.AddScoped<IAccentColorService, AccentColorService>();
builder.Services.AddScoped<IOverlayService, OverlayService>();
builder.Services.AddScoped<GalleryJsInterop>();
builder.Services.AddScoped<IRazorPlaygroundCompiler, UnavailableRazorPlaygroundCompiler>();

var app = builder.Build();
app.UseAntiforgery();
// MapStaticAssets supplies fingerprinted production endpoints. StaticFiles also serves the
// development static-web-asset file provider that Rider uses for project and RCL assets.
app.UseStaticFiles();
app.MapStaticAssets();
app.MapRazorComponents<App>()
.AddAdditionalAssemblies(typeof(MainLayout).Assembly)
.AddInteractiveServerRenderMode();
app.Run();
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
using FluentKit.Sample.Shared.Services;

namespace FluentKit.Sample.Server.Services;

/// <summary>The gallery's in-browser compiler is intentionally unavailable in the Server host.
/// The rest of the shared gallery, including its interactive overlay regressions, remains usable.</summary>
public sealed class UnavailableRazorPlaygroundCompiler : IRazorPlaygroundCompiler
{
public bool IsWarm => true;

public Task WarmUpAsync() => Task.CompletedTask;

public Task<RazorPlaygroundResult> CompileAsync(string razorSource) => Task.FromResult(
RazorPlaygroundResult.Fail(pipelineError: "The live Razor playground is available in the WebAssembly gallery only."));
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,18 @@
</div>
</Example>
</ControlExample>

<ControlExample HeaderText="Overlay-host regression" Description="Type “se” then choose a suggestion. The native label and filtered ancestor reproduce the Interactive Server conditions that require a root overlay host.">
<Example>
<div class="overlay-regression-surface">
<label class="overlay-regression-label">
Server-rendered search
<FluentAutoSuggestBox TValue="string" Items="@_suggestions" @bind-Text="_searchText" Placeholder="Search city..."
SuggestionChosen="@((AutoSuggestBoxItem<string> item) => _chosenVal = item.Value)" />
</label>
</div>
</Example>
</ControlExample>
</div>

@code {
Expand Down
11 changes: 11 additions & 0 deletions samples/FluentKit.Sample.Shared/Pages/Composite/ComboBoxPage.razor
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,17 @@
</div>
</Example>
</ControlExample>

<ControlExample HeaderText="Overlay-host regression" Description="This deliberately places the trigger in a native label and below a filtered surface. In the Interactive Server gallery, choose an item repeatedly: it should select once and stay closed while retaining a blurred acrylic popup.">
<Example>
<div class="overlay-regression-surface">
<label class="overlay-regression-label">
Server-rendered ComboBox
<FluentComboBox TValue="string" Items="@_fruits" @bind-Value="_selectedFruit" Placeholder="Choose fruit" />
</label>
</div>
</Example>
</ControlExample>
</div>

@code {
Expand Down
17 changes: 17 additions & 0 deletions samples/FluentKit.Sample.Shared/wwwroot/gallery.css
Original file line number Diff line number Diff line change
Expand Up @@ -650,3 +650,20 @@ html[data-theme="dark"] .fluent-nav-view-content {
background: rgba(26, 26, 26, 0.3) !important;
}

/* Regression fixture for portalled dropdowns. The filtered ancestor matches the compositing
boundary that breaks an inline acrylic popup; native labels exercise browser label activation. */
.overlay-regression-surface {
max-width: 320px;
padding: var(--spacing-l);
border: 1px solid var(--card-stroke-default);
border-radius: var(--overlay-corner-radius);
background: color-mix(in srgb, var(--card-background-fill) 75%, transparent);
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
}

.overlay-regression-label {
display: grid;
gap: var(--spacing-xs);
color: var(--text-fill-color-secondary);
}
60 changes: 31 additions & 29 deletions src/FluentKit/Composite/AutoSuggestBox/FluentAutoSuggestBox.razor
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
@typeparam TValue
@using FluentKit.Primitives

<div class="fluent-autosuggest-root"
<div @ref="_rootElement"
class="fluent-autosuggest-root"
@onkeydown="OnKeyDownAsync"
@onfocusin="OnFocusIn"
@onfocusout="OnFocusLost">
Expand All @@ -12,7 +13,7 @@
Disabled="@Disabled"
Value="@Text"
ValueChanged="OnTextChangedAsync"
@attributes="AdditionalAttributes"><Buttons>@if (EnableSearchIcon)
@attributes="TextBoxAttributes"><Buttons>@if (EnableSearchIcon)
{
@if (!string.IsNullOrEmpty(Text))
{
Expand All @@ -27,33 +28,34 @@
</FluentTextBoxButton>
}
}</Buttons></FluentTextBox>
</div>

@if (!Disabled && (_open || _closing) && DisplayMatches.Count > 0)
{
<div class="fluent-autosuggest-dismiss-backdrop" @onclick="CloseDropdown"></div>
<div class="fluent-autosuggest-dropdown @(_closing ? "fluent-autosuggest-dropdown--closing" : "")" @ref="_dropdownElement">
<div class="fluent-autosuggest-dropdown-material" aria-hidden="true"></div>
<ul class="fluent-autosuggest-dropdown-list" role="listbox" tabindex="-1">
@for (var i = 0; i < DisplayMatches.Count; i++)
@code {
// Rendered by FluentOverlayHost; keeping it as a Razor fragment preserves component-scoped CSS.
private RenderFragment RenderDropdown => @<ul @ref="_dropdownElement"
id="@DropdownId"
class="fluent-autosuggest-dropdown-list"
role="listbox"
tabindex="-1"
@onkeydown="OnKeyDownAsync">
@for (var i = 0; i < DisplayMatches.Count; i++)
{
var item = DisplayMatches[i];
var isHighlighted = i == _highlightedIndex;
<li class="fluent-autosuggest-item @(isHighlighted ? "fluent-autosuggest-item--highlighted" : null) @(item.Disabled ? "fluent-autosuggest-item--disabled" : null)"
role="option"
aria-selected="@isHighlighted"
@onmousedown:preventDefault="true"
@onclick="() => ChooseAsync(item)">
@if (ItemTemplate is not null)
{
var item = DisplayMatches[i];
var isHighlighted = i == _highlightedIndex;
<li class="fluent-autosuggest-item @(isHighlighted ? "fluent-autosuggest-item--highlighted" : null) @(item.Disabled ? "fluent-autosuggest-item--disabled" : null)"
role="option"
aria-selected="@isHighlighted"
@onmousedown:preventDefault="true"
@onclick="() => ChooseAsync(item)">
@if (ItemTemplate is not null)
{
@ItemTemplate(item)
}
else
{
<span>@item.Name</span>
}
</li>
@ItemTemplate(item)
}
</ul>
</div>
}
</div>
else
{
<span>@item.Name</span>
}
</li>
}
</ul>;
}
Loading
Loading