Skip to content

Commit 12ad5c3

Browse files
authored
Create hover tokens for grid row hover effect (#4892)
1 parent 85802db commit 12ad5c3

File tree

10 files changed

+148
-19
lines changed

10 files changed

+148
-19
lines changed

src/Aspire.Dashboard/Components/Controls/PropertyGrid.razor

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@
66
ResizableColumns="true"
77
Style="width:100%"
88
GenerateHeader="GenerateHeaderOption.Sticky"
9-
GridTemplateColumns="@GridTemplateColumns">
9+
GridTemplateColumns="@GridTemplateColumns"
10+
ShowHover="true">
1011
<TemplateColumn Title="@(NameColumnTitle ?? Loc[nameof(ControlsStrings.NameColumnHeader)])" Class="nameColumn" SortBy="@NameSort" Sortable="@IsNameSortable">
1112
<GridValue Value="@NameColumnValue(context)" HighlightText="@HighlightText" />
1213
</TemplateColumn>

src/Aspire.Dashboard/Components/Controls/ResourceDetails.razor

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,8 @@
4747
ResizableColumns="true"
4848
Style="width:100%"
4949
GenerateHeader="GenerateHeaderOption.Sticky"
50-
GridTemplateColumns="1fr 1.5fr">
50+
GridTemplateColumns="1fr 1.5fr"
51+
ShowHover="true">
5152
<TemplateColumn Title="@ControlStringsLoc[nameof(ControlsStrings.NameColumnHeader)]" Class="nameColumn">
5253
<GridValue Value="@(context.KnownProperty?.DisplayName ?? context.Key)" ToolTip="@context.Key" />
5354
</TemplateColumn>
@@ -66,7 +67,8 @@
6667
ResizableColumns="true"
6768
Style="width:100%"
6869
GenerateHeader="GenerateHeaderOption.Sticky"
69-
GridTemplateColumns="1fr 1.5fr">
70+
GridTemplateColumns="1fr 1.5fr"
71+
ShowHover="true">
7072
<TemplateColumn Title="@ControlStringsLoc[nameof(ControlsStrings.NameColumnHeader)]" Class="nameColumn">
7173
<GridValue Value="@context.Name" />
7274
</TemplateColumn>

src/Aspire.Dashboard/Components/Pages/Resources.razor

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@
7474
@{
7575
var gridTemplateColumns = HasResourcesWithCommands ? "1fr 1.5fr 1.25fr 1.5fr 2.5fr 2.5fr 1fr 1fr 1fr" : "1fr 1.5fr 1.25fr 1.5fr 2.5fr 2.5fr 1fr 1fr";
7676
}
77-
<FluentDataGrid Virtualize="true" GenerateHeader="GenerateHeaderOption.Sticky" ItemSize="46" Items="@FilteredResources" ResizableColumns="true" GridTemplateColumns="@gridTemplateColumns" RowClass="GetRowClass" Loading="_isLoading">
77+
<FluentDataGrid Virtualize="true" GenerateHeader="GenerateHeaderOption.Sticky" ItemSize="46" Items="@FilteredResources" ResizableColumns="true" GridTemplateColumns="@gridTemplateColumns" RowClass="GetRowClass" Loading="_isLoading" ShowHover="true">
7878
<ChildContent>
7979
<PropertyColumn Title="@Loc[nameof(Dashboard.Resources.Resources.ResourcesTypeColumnHeader)]" Property="@(c => c.ResourceType)" Sortable="true" Tooltip="true" TooltipText="@(c => c.ResourceType)"/>
8080
<TemplateColumn Title="@ControlsStringsLoc[nameof(ControlsStrings.NameColumnHeader)]" Sortable="true" SortBy="@_nameSort" Tooltip="true" TooltipText="@(c => GetResourceName(c))" >

src/Aspire.Dashboard/Components/Pages/StructuredLogs.razor

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@
104104
<Summary>
105105
<div class="logs-summary-layout">
106106
<div class="logs-grid-container continuous-scroll-overflow">
107-
<FluentDataGrid Virtualize="true" RowClass="@GetRowClass" GenerateHeader="GenerateHeaderOption.Sticky" ItemSize="46" ResizableColumns="true" ItemsProvider="@GetData" TGridItem="OtlpLogEntry" GridTemplateColumns="1fr 1fr 1fr 5fr 0.8fr 0.8fr">
107+
<FluentDataGrid Virtualize="true" RowClass="@GetRowClass" GenerateHeader="GenerateHeaderOption.Sticky" ItemSize="46" ResizableColumns="true" ItemsProvider="@GetData" TGridItem="OtlpLogEntry" GridTemplateColumns="1fr 1fr 1fr 5fr 0.8fr 0.8fr" ShowHover="true">
108108
<ChildContent>
109109
<TemplateColumn Title="@Loc[nameof(Dashboard.Resources.StructuredLogs.StructuredLogsResourceColumnHeader)]" Tooltip="true" TooltipText="@(e => GetResourceName(e.Application))">
110110
<span style="padding-left:5px; border-left-width: 5px; border-left-style: solid; border-left-color: @(ColorGenerator.Instance.GetColorHexByKey(GetResourceName(context.Application)));">

src/Aspire.Dashboard/Components/Pages/TraceDetail.razor

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@
5959
</div>
6060
</DetailsTitleTemplate>
6161
<Summary>
62-
<FluentDataGrid Virtualize="true" GenerateHeader="GenerateHeaderOption.Sticky" Class="trace-view-grid" ResizableColumns="true" ItemsProvider="@GetData" TGridItem="SpanWaterfallViewModel" RowClass="@GetRowClass" GridTemplateColumns="4fr 12fr 85px">
62+
<FluentDataGrid Virtualize="true" GenerateHeader="GenerateHeaderOption.Sticky" Class="trace-view-grid" ResizableColumns="true" ItemsProvider="@GetData" TGridItem="SpanWaterfallViewModel" RowClass="@GetRowClass" GridTemplateColumns="4fr 12fr 85px" ShowHover="true">
6363
<TemplateColumn Title="Name">
6464
<div class="col-long-content" title="@context.GetTooltip(_applications)" @onclick="() => OnShowPropertiesAsync(context, null)">
6565
@{

src/Aspire.Dashboard/Components/Pages/TraceDetail.razor.css

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,7 @@
2121
display: inline !important;
2222
}
2323

24-
::deep .trace-view-grid fluent-data-grid-row[row-type="default"]:hover {
25-
background-color: var(--neutral-fill-hover);
26-
}
27-
28-
::deep .trace-view-grid fluent-data-grid-row[row-type="default"] {
24+
::deep fluent-data-grid.trace-view-grid fluent-data-grid-row[row-type="default"]:hover {
2925
cursor: pointer;
3026
}
3127

src/Aspire.Dashboard/Components/Pages/Traces.razor

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131
</ToolbarSection>
3232
<MainSection>
3333
<div class="datagrid-overflow-area continuous-scroll-overflow" tabindex="-1">
34-
<FluentDataGrid Virtualize="true" GenerateHeader="GenerateHeaderOption.Sticky" ItemSize="46" ResizableColumns="true" ItemsProvider="@GetData" TGridItem="OtlpTrace" GridTemplateColumns="0.8fr 2fr 3fr 0.8fr 0.5fr">
34+
<FluentDataGrid Virtualize="true" GenerateHeader="GenerateHeaderOption.Sticky" ItemSize="46" ResizableColumns="true" ItemsProvider="@GetData" TGridItem="OtlpTrace" GridTemplateColumns="0.8fr 2fr 3fr 0.8fr 0.5fr" ShowHover="true">
3535
<ChildContent>
3636
<TemplateColumn Title="@ControlsStringsLoc[nameof(ControlsStrings.TimestampColumnHeader)]" TooltipText="@(context => FormatHelpers.FormatDateTime(TimeProvider, context.FirstSpan.StartTime, MillisecondsDisplay.Full, CultureInfo.CurrentCulture))" Tooltip="true">
3737
@FormatHelpers.FormatTimeWithOptionalDate(TimeProvider, context.FirstSpan.StartTime, MillisecondsDisplay.Truncated)

src/Aspire.Dashboard/wwwroot/css/app.css

Lines changed: 45 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,13 @@ fluent-toolbar[orientation=horizontal] {
5151
--messagebar-info-border-color: #d1d1d1;
5252

5353
--layout-toolbar-padding: calc(var(--design-unit) * 1.5px);
54+
55+
/*
56+
--neutral-layer-2 is the background for the body of most of the site so
57+
we can default the datagrid hover color to that and just override it as
58+
necessary elsewhere (e.g. in the details view)
59+
*/
60+
--datagrid-hover-color: var(--neutral-layer-2-hover);
5461
}
5562

5663
[data-theme="dark"] {
@@ -77,6 +84,17 @@ fluent-toolbar[orientation=horizontal] {
7784
color-scheme: dark;
7885
}
7986

87+
/*
88+
Override the `cursor: pointer` in the default styles from fluentui-blazor since
89+
we don't have selection implemented and the pointer doesn't really make sense.
90+
The `fluent-data-grid-row[role='row']` part added to the beginning is a bit of
91+
a hack to get the specificity higher than the default style to avoid using
92+
`!important` just in case there's a reason to to override further later.
93+
*/
94+
fluent-data-grid-row[role='row'].hover:not([row-type='header'],[row-type='sticky-header']):hover {
95+
cursor: auto;
96+
}
97+
8098
[data-theme="light"] {
8199
color-scheme: light;
82100
}
@@ -338,6 +356,24 @@ fluent-switch.table-switch::part(label) {
338356
width: 160px;
339357
}
340358

359+
/*
360+
If ShowHover is enabled on a FluentDataGrid, we need to tweak the way anchors and buttons
361+
are colored when a row is hovered
362+
*/
363+
fluent-data-grid-row[row-type=default].hover:hover fluent-anchor[appearance=lightweight]::part(control):not(:hover),
364+
fluent-data-grid-row[row-type=default].hover:hover fluent-anchor[appearance=stealth]::part(control):not(:hover),
365+
fluent-data-grid-row[row-type=default].hover:hover fluent-button[appearance=lightweight]::part(control):not(:hover),
366+
fluent-data-grid-row[row-type=default].hover:hover fluent-button[appearance=stealth]::part(control):not(:hover) {
367+
background-color: var(--datagrid-hover-color);
368+
}
369+
370+
fluent-data-grid-row[row-type=default].hover:hover fluent-anchor[appearance=lightweight]::part(control):hover,
371+
fluent-data-grid-row[row-type=default].hover:hover fluent-anchor[appearance=stealth]::part(control):hover,
372+
fluent-data-grid-row[row-type=default].hover:hover fluent-button[appearance=lightweight]::part(control):hover,
373+
fluent-data-grid-row[row-type=default].hover:hover fluent-button[appearance=stealth]::part(control):hover {
374+
background-color: var(--fill-color);
375+
}
376+
341377
.property-grid-container {
342378
grid-area: main;
343379
overflow: auto;
@@ -352,8 +388,12 @@ fluent-switch.table-switch::part(label) {
352388
--fill-color: var(--neutral-layer-1);
353389
}
354390

391+
.property-grid-container fluent-accordion fluent-data-grid {
392+
--datagrid-hover-color: var(--neutral-layer-1-hover);
393+
}
394+
355395
/* Under the current light theme, --neutral-layer-1 is too white, so we'll
356-
use something that's halfway between --neutral-layer-2 and --neutral-layer1.
396+
use something that's halfway between --neutral-layer-2 and --neutral-layer-1.
357397
*/
358398
[data-theme="light"] .property-grid-container fluent-accordion {
359399
--fill-color: var(--neutral-fill-rest);
@@ -385,13 +425,13 @@ fluent-switch.table-switch::part(label) {
385425
border-bottom: none;
386426
}
387427

388-
.property-grid-container fluent-accordion fluent-data-grid fluent-button[appearance=stealth]:not(:hover)::part(control),
389-
.property-grid-container fluent-accordion fluent-data-grid fluent-button[appearance=lightweight]:not(:hover)::part(control) {
428+
.property-grid-container fluent-accordion fluent-data-grid-row fluent-button[appearance=stealth]::part(control):not(:hover),
429+
.property-grid-container fluent-accordion fluent-data-grid-row fluent-button[appearance=lightweight]::part(control):not(:hover) {
390430
background-color: var(--fill-color);
391431
}
392432

393-
.property-grid-container fluent-accordion fluent-data-grid fluent-button[appearance=stealth]:hover::part(control),
394-
.property-grid-container fluent-accordion fluent-data-grid fluent-button[appearance=lightweight]:hover::part(control) {
433+
.property-grid-container fluent-accordion fluent-data-grid-row:not([row-type=default]) fluent-button[appearance=stealth]::part(control):hover,
434+
.property-grid-container fluent-accordion fluent-data-grid-row:not([row-type=default]) fluent-button[appearance=lightweight]::part(control):hover {
395435
background-color: var(--neutral-fill-stealth-hover-on-neutral-fill-layer-rest);
396436
}
397437

src/Aspire.Dashboard/wwwroot/js/app-theme.js

Lines changed: 91 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,10 @@ import {
33
baseLayerLuminance,
44
SwatchRGB,
55
fillColor,
6-
neutralLayerL2
6+
neutralLayerL2,
7+
neutralPalette,
8+
DesignToken,
9+
neutralFillLayerRestDelta
710
} from "/_content/Microsoft.FluentUI.AspNetCore.Components/Microsoft.FluentUI.AspNetCore.Components.lib.module.js";
811

912
const currentThemeCookieName = "currentTheme";
@@ -12,6 +15,7 @@ const themeSettingDark = "Dark";
1215
const themeSettingLight = "Light";
1316
const darkThemeLuminance = 0.19;
1417
const lightThemeLuminance = 1.0;
18+
const darknessLuminanceTarget = (-0.1 + Math.sqrt(0.21)) / 2;
1519

1620
/**
1721
* Updates the current theme on the site based on the specified theme
@@ -160,11 +164,97 @@ function applyTheme(theme) {
160164
setThemeOnDocument(theme);
161165
}
162166

167+
/**
168+
*
169+
* @param {Palette} palette
170+
* @param {number} baseLayerLuminance
171+
* @returns {number}
172+
*/
173+
function neutralLayer1Index(palette, baseLayerLuminance) {
174+
return palette.closestIndexOf(SwatchRGB.create(baseLayerLuminance, baseLayerLuminance, baseLayerLuminance));
175+
}
176+
177+
/**
178+
*
179+
* @param {Palette} palette
180+
* @param {Swatch} reference
181+
* @param {number} baseLayerLuminance
182+
* @param {number} layerDelta
183+
* @param {number} hoverDeltaLight
184+
* @param {number} hoverDeltaDark
185+
* @returns {Swatch}
186+
*/
187+
function neutralLayerHoverAlgorithm(palette, reference, baseLayerLuminance, layerDelta, hoverDeltaLight, hoverDeltaDark) {
188+
const baseIndex = neutralLayer1Index(palette, baseLayerLuminance);
189+
// Determine both the size of the delta (from the value passed in) and the direction (if the current color is dark,
190+
// the hover color will be a lower index (lighter); if the current color is light, the hover color will be a higher index (darker))
191+
const hoverDelta = isDark(reference) ? hoverDeltaDark * -1 : hoverDeltaLight;
192+
return palette.get(baseIndex + (layerDelta * -1) + hoverDelta);
193+
}
194+
195+
/**
196+
*
197+
* @param {Swatch} color
198+
* @returns {boolean}
199+
*/
200+
function isDark(color) {
201+
return color.relativeLuminance <= darknessLuminanceTarget;
202+
}
203+
204+
/**
205+
* Creates additional design tokens that are used to define the hover colors for the neutral layers
206+
* used in the site theme (neutral-layer-1 and neutral-layer-2, specifically). Unlike other -hover
207+
* variants, these are not created by the design system by default so we need to create them ourselves.
208+
* This is a lightly tweaked variant of other hover recipes used in the design system.
209+
*/
210+
function createAdditionalDesignTokens() {
211+
const neutralLayer1HoverLightDelta = DesignToken.create({ name: 'neutral-layer-1-hover-light-delta', cssCustomPropertyName: null }).withDefault(3);
212+
const neutralLayer1HoverDarkDelta = DesignToken.create({ name: 'neutral-layer-1-hover-dark-delta', cssCustomPropertyName: null }).withDefault(2);
213+
const neutralLayer2HoverLightDelta = DesignToken.create({ name: 'neutral-layer-2-hover-light-delta', cssCustomPropertyName: null }).withDefault(2);
214+
const neutralLayer2HoverDarkDelta = DesignToken.create({ name: 'neutral-layer-2-hover-dark-delta', cssCustomPropertyName: null }).withDefault(2);
215+
216+
const neutralLayer1HoverRecipe = DesignToken.create({ name: 'neutral-layer-1-hover-recipe', cssCustomPropertyName: null }).withDefault({
217+
evaluate: (element, reference) =>
218+
neutralLayerHoverAlgorithm(
219+
neutralPalette.getValueFor(element),
220+
reference || fillColor.getValueFor(element),
221+
baseLayerLuminance.getValueFor(element),
222+
0, // No layer delta since this is for neutral-layer-1
223+
neutralLayer1HoverLightDelta.getValueFor(element),
224+
neutralLayer1HoverDarkDelta.getValueFor(element)
225+
),
226+
});
227+
228+
const neutralLayer2HoverRecipe = DesignToken.create({ name: 'neutral-layer-2-hover-recipe', cssCustomPropertyName: null }).withDefault({
229+
evaluate: (element, reference) =>
230+
neutralLayerHoverAlgorithm(
231+
neutralPalette.getValueFor(element),
232+
reference || fillColor.getValueFor(element),
233+
baseLayerLuminance.getValueFor(element),
234+
// Use the same layer delta used by the base recipe to calculate layer 2
235+
neutralFillLayerRestDelta.getValueFor(element),
236+
neutralLayer2HoverLightDelta.getValueFor(element),
237+
neutralLayer2HoverDarkDelta.getValueFor(element)
238+
),
239+
});
240+
241+
// Creates the --neutral-layer-1-hover custom CSS property
242+
DesignToken.create('neutral-layer-1-hover').withDefault((element) =>
243+
neutralLayer1HoverRecipe.getValueFor(element).evaluate(element),
244+
);
245+
246+
// Creates the --neutral-layer-2-hover custom CSS property
247+
DesignToken.create('neutral-layer-2-hover').withDefault((element) =>
248+
neutralLayer2HoverRecipe.getValueFor(element).evaluate(element),
249+
);
250+
}
251+
163252
function initializeTheme() {
164253
const themeCookieValue = getThemeCookieValue();
165254
const effectiveTheme = getEffectiveTheme(themeCookieValue);
166255

167256
applyTheme(effectiveTheme);
168257
}
169258

259+
createAdditionalDesignTokens();
170260
initializeTheme();

tests/Aspire.Workload.Tests/WorkloadTestsBase.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ protected static async Task<ResourceRow[]> CheckDashboardHasResourcesAsync(IPage
6161
await Task.Delay(500);
6262

6363
// _testOutput.WriteLine($"Checking for rows again");
64-
ILocator rowsLocator = dashboardPage.Locator("//fluent-data-grid-row[@class='resource-row']");
64+
ILocator rowsLocator = dashboardPage.Locator("//fluent-data-grid-row[@class='hover resource-row']");
6565
var allRows = await rowsLocator.AllAsync();
6666
// _testOutput.WriteLine($"found rows#: {allRows.Count}");
6767
if (allRows.Count == 0)

0 commit comments

Comments
 (0)