diff --git a/src/BlazorUI/Bit.BlazorUI/Components/Navs/Nav/BitNav.scss b/src/BlazorUI/Bit.BlazorUI/Components/Navs/Nav/BitNav.scss
index 29992be734a..c8b683f7385 100644
--- a/src/BlazorUI/Bit.BlazorUI/Components/Navs/Nav/BitNav.scss
+++ b/src/BlazorUI/Bit.BlazorUI/Components/Navs/Nav/BitNav.scss
@@ -9,9 +9,16 @@
// --bit-Nav-disabled-color everything a disabled item draws (default: $clr-fg-dis)
// --bit-Nav-hover-background background of a hovered item (default: $clr-bg-pri-hover)
// --bit-Nav-hover-color text and chevron color of a hovered item (default: $clr-fg-pri)
+// --bit-Nav-hover-icon-color icon color of a hovered item (default: --bit-Nav-icon-color)
+// --bit-Nav-hover-description-color description color of a hovered item (default: --bit-Nav-description-color)
// --bit-Nav-pressed-background background of a pressed item (default: $clr-bg-pri-active)
+// --bit-Nav-pressed-color text and chevron color of a pressed item (default: --bit-Nav-hover-color; $clr-fg-pri on touch)
+// --bit-Nav-pressed-icon-color icon color of a pressed item (default: --bit-Nav-hover-icon-color; --bit-Nav-icon-color on touch)
+// --bit-Nav-pressed-description-color description color of a pressed item (default: --bit-Nav-hover-description-color; --bit-Nav-description-color on touch)
// --bit-Nav-selected-background background of the selected item (default: $clr-bg-pri-active)
// --bit-Nav-selected-color text and chevron color of the selected item (default: $clr-fg-pri)
+// --bit-Nav-selected-icon-color icon color of the selected item (default: --bit-Nav-icon-color)
+// --bit-Nav-selected-description-color description color of the selected item (default: --bit-Nav-description-color)
// --bit-Nav-indicator-color leading bar of the selected item (default: $clr-pri)
// --bit-Nav-indicator-width thickness of that bar (default: $shp-border-width-thick)
// --bit-Nav-item-radius corner radius of an item (default: $shp-radius-control)
@@ -29,11 +36,20 @@
// --bit-Nav-separator-color separator color (default: $clr-brd-sec)
//
// A parameter written on the nav wins over the variable that restyles what it sets: an explicit Color over the
-// icon and indicator colors, an explicit Accent over the hover, pressed and selected backgrounds and the colors
-// drawn on them (a semantic Accent over the icon and description colors of an accented item too), and an explicit
-// Size over the font, icon, description and header sizes and the item and header heights. The variables restyle
-// the default, not a choice - which is why Color, Accent and Size publish nothing while they are left unset: a
-// class that always carried the default would be indistinguishable from one that was asked for.
+// icon and indicator colors, an explicit Accent over the hover, pressed and selected backgrounds and every color
+// drawn on them - the text, the chevron, the icon and the description - and an explicit Size over the font, icon,
+// description and header sizes and the item and header heights. The variables restyle the default, not a choice -
+// which is why Color, Accent and Size publish nothing while they are left unset: a class that always carried the
+// default would be indistinguishable from one that was asked for.
+
+// The colors the parts of an item have at rest, which the hovered, pressed and selected ones fall back on.
+$nav-icon-color: var(--bit-Nav-icon-color, #{$clr-pri});
+$nav-description-color: var(--bit-Nav-description-color, #{$clr-fg-ter});
+
+// The hovered ones, which a press falls back on where the pointer can hover (see .bit-nav-ict).
+$nav-hover-color: var(--bit-Nav-hover-color, #{$clr-fg-pri});
+$nav-hover-icon-color: var(--bit-Nav-hover-icon-color, #{$nav-icon-color});
+$nav-hover-description-color: var(--bit-Nav-hover-description-color, #{$nav-description-color});
// The accent fills the whole hovered or selected item, so everything drawn on top of it moves to the
// accent's own colors at once. Each part had been given a color that reads on the page background
@@ -43,16 +59,18 @@
// its color, so the rules that declare the colors are the ones at rest - the parts' own rules and the selection's -
// and a part the app paints through Classes keeps that paint under the pointer, instead of losing it to a state
// that outranks the app's class. Each part unsets the variable on itself at rest (see .bit-nav-itm).
-@mixin accented-item-content($color, $state: false) {
- // A semantic Accent publishes these; any other leaves the icon and the description in the colors they have
- // at rest, ranked the same way as they are there.
- $icon: var(--bit-nav-clr-icon, var(--bit-nav-clr, var(--bit-Nav-icon-color, #{$clr-pri})));
- $des: var(--bit-nav-clr-des, var(--bit-Nav-description-color, #{$clr-fg-ter}));
+// $color, $icon-color and $description-color are the state's public variables for those parts, so a theme that
+// gives the state a vivid fill can move all three with it; an explicit Accent publishes the private ones read
+// before them, and an explicit Color the icon's.
+@mixin accented-item-content($color, $icon-color, $description-color, $state: false) {
+ $text: var(--bit-nav-clr-text, #{$color});
+ $icon: var(--bit-nav-clr-icon, var(--bit-nav-clr, #{$icon-color}));
+ $des: var(--bit-nav-clr-des, #{$description-color});
@if $state {
.bit-nav-itm,
.bit-nav-cbt {
- --bit-nav-sfg: #{$color};
+ --bit-nav-sfg: #{$text};
}
.bit-nav-iic {
@@ -65,7 +83,7 @@
} @else {
.bit-nav-itm,
.bit-nav-cbt {
- color: var(--bit-nav-sfg, #{$color});
+ color: var(--bit-nav-sfg, #{$text});
}
.bit-nav-iic {
@@ -287,16 +305,33 @@ $nav-private-properties: item-font-size, item-min-height, icon-size, des-font-si
&:hover {
--bit-nav-sbg: var(--bit-nav-clr-hover, var(--bit-Nav-hover-background, #{$clr-bg-pri-hover}));
- @include accented-item-content(var(--bit-nav-clr-text, var(--bit-Nav-hover-color, #{$clr-fg-pri})), $state: true);
+ @include accented-item-content($nav-hover-color, $nav-hover-icon-color, $nav-hover-description-color, $state: true);
}
}
// Unlike the hover, the pressed state is not kept to the pointers that can hover: on a touch screen it is
- // the one feedback a tap gets before the page changes.
+ // the one feedback a tap gets before the page changes. There no hover ever applies, so the pressed colors fall
+ // back on the ones at rest rather than on a hover's that a theme set for a fill the tap never shows.
&:active {
--bit-nav-sbg: var(--bit-nav-clr-active, var(--bit-Nav-pressed-background, #{$clr-bg-pri-active}));
- @include accented-item-content(var(--bit-nav-clr-text, var(--bit-Nav-hover-color, #{$clr-fg-pri})), $state: true);
+ @include accented-item-content(
+ var(--bit-Nav-pressed-color, #{$clr-fg-pri}),
+ var(--bit-Nav-pressed-icon-color, #{$nav-icon-color}),
+ var(--bit-Nav-pressed-description-color, #{$nav-description-color}),
+ $state: true);
+ }
+
+ // Where the pointer can hover, a press is a hover too, so the pressed colors fall back on the hovered ones the
+ // item is already drawn in. Declared after the rule above at the same weight, so it wins wherever it applies.
+ @media (hover: hover) {
+ &:active {
+ @include accented-item-content(
+ var(--bit-Nav-pressed-color, #{$nav-hover-color}),
+ var(--bit-Nav-pressed-icon-color, #{$nav-hover-icon-color}),
+ var(--bit-Nav-pressed-description-color, #{$nav-hover-description-color}),
+ $state: true);
+ }
}
}
@@ -349,7 +384,7 @@ $nav-private-properties: item-font-size, item-min-height, icon-size, des-font-si
text-overflow: ellipsis;
padding-inline-start: spacing(2.75);
--bit-nav-sfg: initial;
- color: var(--bit-nav-sfg, var(--bit-Nav-description-color, #{$clr-fg-ter}));
+ color: var(--bit-nav-sfg, #{$nav-description-color});
font-size: var(--bit-nav-des-font-size, var(--bit-Nav-description-font-size, #{$tg-fs-xs}));
transition: color $mot-duration-short $mot-easing;
}
@@ -377,7 +412,7 @@ $nav-private-properties: item-font-size, item-min-height, icon-size, des-font-si
line-height: 1;
flex-shrink: 0;
--bit-nav-sfg: initial;
- color: var(--bit-nav-sfg, var(--bit-nav-clr, var(--bit-Nav-icon-color, #{$clr-pri})));
+ color: var(--bit-nav-sfg, var(--bit-nav-clr, #{$nav-icon-color}));
font-size: var(--bit-nav-icon-size, var(--bit-Nav-icon-size, #{$siz-icon-md}));
transition: color $mot-duration-short $mot-easing;
}
@@ -416,7 +451,10 @@ $nav-private-properties: item-font-size, item-min-height, icon-size, des-font-si
border-color: var(--bit-nav-clr, var(--bit-Nav-indicator-color, #{$clr-pri}));
background-color: var(--bit-nav-sbg, var(--bit-nav-clr-active, var(--bit-Nav-selected-background, #{$clr-bg-pri-active})));
- @include accented-item-content(var(--bit-nav-clr-text, var(--bit-Nav-selected-color, #{$clr-fg-pri})));
+ @include accented-item-content(
+ var(--bit-Nav-selected-color, #{$clr-fg-pri}),
+ var(--bit-Nav-selected-icon-color, #{$nav-icon-color}),
+ var(--bit-Nav-selected-description-color, #{$nav-description-color}));
.bit-nav-itm {
font-weight: $tg-fw-semibold;
@@ -496,10 +534,14 @@ $nav-private-properties: item-font-size, item-min-height, icon-size, des-font-si
// follow the accent's on-color together with the text.
--bit-nav-clr-icon: #{role($tokens, on)};
--bit-nav-clr-des: #{role($tokens, on)};
+ } @else {
+ // A surface accent only tints the item's background, so the icon and the description keep the colors
+ // they have at rest on it - published all the same, so they are read ahead of the hovered, pressed and
+ // selected variables the way the accent's text color is, which a theme sets for its own fill rather than
+ // for this one (see accented-item-content). The icon still follows an explicit Color.
+ --bit-nav-clr-icon: var(--bit-nav-clr, #{$nav-icon-color});
+ --bit-nav-clr-des: #{$nav-description-color};
}
- // A surface accent only tints the item's background, so it publishes no icon and description colors:
- // left unset, they keep the colors they have at rest, the public variables of those two included (see
- // accented-item-content).
}
}
diff --git a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Navs/Nav/BitNavDemo.razor.cs b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Navs/Nav/BitNavDemo.razor.cs
index b2513fddf4c..61cf04aba2c 100644
--- a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Navs/Nav/BitNavDemo.razor.cs
+++ b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Navs/Nav/BitNavDemo.razor.cs
@@ -312,13 +312,13 @@ public partial class BitNavDemo
{
Name = "--bit-Nav-icon-color",
DefaultValue = "--bit-clr-pri",
- Description = "Icon color of an item at rest; kept on hover and selection unless the Accent is a semantic role. The Color parameter wins over it.",
+ Description = "Icon color of an item at rest, and of a hovered, pressed or selected one its own variable leaves unset (and of one under an explicit surface Accent). The Color parameter wins over it.",
},
new()
{
Name = "--bit-Nav-description-color",
DefaultValue = "--bit-clr-fg-ter",
- Description = "Color of the description of an item.",
+ Description = "Description color of an item at rest, and of a hovered, pressed or selected one its own variable leaves unset (and of one under an explicit surface Accent).",
},
new()
{
@@ -339,12 +339,42 @@ public partial class BitNavDemo
Description = "Text and chevron color of a hovered item. The Accent parameter wins over it.",
},
new()
+ {
+ Name = "--bit-Nav-hover-icon-color",
+ DefaultValue = "--bit-Nav-icon-color, then --bit-clr-pri",
+ Description = "Icon color of a hovered item (pointer devices only). The Accent and Color parameters win over it.",
+ },
+ new()
+ {
+ Name = "--bit-Nav-hover-description-color",
+ DefaultValue = "--bit-Nav-description-color, then --bit-clr-fg-ter",
+ Description = "Description color of a hovered item (pointer devices only). The Accent parameter wins over it.",
+ },
+ new()
{
Name = "--bit-Nav-pressed-background",
DefaultValue = "--bit-clr-bg-pri-active",
Description = "Background of an item while it is pressed (the one feedback a tap gets on a touch screen). The Accent parameter wins over it.",
},
new()
+ {
+ Name = "--bit-Nav-pressed-color",
+ DefaultValue = "--bit-Nav-hover-color, then --bit-clr-fg-pri (on a touch screen --bit-clr-fg-pri)",
+ Description = "Text and chevron color of an item while it is pressed. Where the pointer can hover it falls back on the hovered color; on a touch screen, where no hover applies, on the default. The Accent parameter wins over it.",
+ },
+ new()
+ {
+ Name = "--bit-Nav-pressed-icon-color",
+ DefaultValue = "--bit-Nav-hover-icon-color, then --bit-Nav-icon-color, then --bit-clr-pri (on a touch screen --bit-Nav-icon-color, then --bit-clr-pri)",
+ Description = "Icon color of an item while it is pressed. Where the pointer can hover it falls back on the hovered icon color; on a touch screen, where no hover applies, on the one at rest. The Accent and Color parameters win over it.",
+ },
+ new()
+ {
+ Name = "--bit-Nav-pressed-description-color",
+ DefaultValue = "--bit-Nav-hover-description-color, then --bit-Nav-description-color, then --bit-clr-fg-ter (on a touch screen --bit-Nav-description-color, then --bit-clr-fg-ter)",
+ Description = "Description color of an item while it is pressed. Where the pointer can hover it falls back on the hovered description color; on a touch screen, where no hover applies, on the one at rest. The Accent parameter wins over it.",
+ },
+ new()
{
Name = "--bit-Nav-selected-background",
DefaultValue = "--bit-clr-bg-pri-active",
@@ -357,6 +387,18 @@ public partial class BitNavDemo
Description = "Text and chevron color of the selected item, and of a collapsed branch that holds it. The Accent parameter wins over it.",
},
new()
+ {
+ Name = "--bit-Nav-selected-icon-color",
+ DefaultValue = "--bit-Nav-icon-color, then --bit-clr-pri",
+ Description = "Icon color of the selected item, and of a collapsed branch that holds it - the one to move with --bit-Nav-selected-background when that is a vivid fill. The Accent and Color parameters win over it: an explicit Accent draws the icon in its own colors on its own fill (a semantic one in its on-color, a surface one in the icon color at rest), so a value set for the theme's fill never lands on the Accent's.",
+ },
+ new()
+ {
+ Name = "--bit-Nav-selected-description-color",
+ DefaultValue = "--bit-Nav-description-color, then --bit-clr-fg-ter",
+ Description = "Description color of the selected item, and of a collapsed branch that holds it. The Accent parameter wins over it.",
+ },
+ new()
{
Name = "--bit-Nav-indicator-color",
DefaultValue = "--bit-clr-pri",
diff --git a/src/BlazorUI/Tests/Bit.BlazorUI.Tests/Components/Navs/Nav/BitNavStylesheetTests.cs b/src/BlazorUI/Tests/Bit.BlazorUI.Tests/Components/Navs/Nav/BitNavStylesheetTests.cs
index 22abc88b3cd..e8c4dd4a6fd 100644
--- a/src/BlazorUI/Tests/Bit.BlazorUI.Tests/Components/Navs/Nav/BitNavStylesheetTests.cs
+++ b/src/BlazorUI/Tests/Bit.BlazorUI.Tests/Components/Navs/Nav/BitNavStylesheetTests.cs
@@ -87,7 +87,7 @@ public void BitNavShouldLetAParameterWinOverItsPublicVariable()
// So does an explicit Color for the icon and the indicator, and an explicit Accent for the hover, the press
// and the selection; an unset Accent is the PrimaryBackground one.
// (The states set the state variables the rules at rest read first, rather than painting the colors themselves.)
- StringAssert.Contains(stylesheet, "color: var(--bit-nav-sfg, var(--bit-nav-clr, var(--bit-Nav-icon-color, #{$clr-pri})));");
+ StringAssert.Contains(stylesheet, "color: var(--bit-nav-sfg, var(--bit-nav-clr, #{$nav-icon-color}));");
StringAssert.Contains(stylesheet, "border-color: var(--bit-nav-clr, var(--bit-Nav-indicator-color, #{$clr-pri}));");
StringAssert.Contains(stylesheet, "--bit-nav-sbg: var(--bit-nav-clr-hover, var(--bit-Nav-hover-background, #{$clr-bg-pri-hover}));");
StringAssert.Contains(stylesheet, "background-color: var(--bit-nav-sbg, var(--bit-nav-clr-active, var(--bit-Nav-selected-background, #{$clr-bg-pri-active})));");
@@ -95,6 +95,68 @@ public void BitNavShouldLetAParameterWinOverItsPublicVariable()
Assert.IsFalse(Regex.IsMatch(stylesheet, @"var\(--bit-Nav-[a-z-]+, var\(--bit-nav-"), "A public variable is read before the parameter it restyles the default of.");
}
+ [TestMethod]
+ public void BitNavShouldLetEveryStateRecolorTheIconAndTheDescriptionOfItsItem()
+ {
+ var stylesheet = ReadStylesheet();
+
+ // A theme that gives a state a vivid fill moves the text with --bit-Nav-*-color; without a variable of their own
+ // the icon and the description stayed in their colors at rest, unreadable on that fill. Each falls back on the
+ // one at rest, so a nav that sets none of them looks as it did.
+ StringAssert.Contains(stylesheet, "$nav-icon-color: var(--bit-Nav-icon-color, #{$clr-pri});");
+ StringAssert.Contains(stylesheet, "$nav-description-color: var(--bit-Nav-description-color, #{$clr-fg-ter});");
+ StringAssert.Contains(stylesheet, "$nav-hover-color: var(--bit-Nav-hover-color, #{$clr-fg-pri});");
+ StringAssert.Contains(stylesheet, "$nav-hover-icon-color: var(--bit-Nav-hover-icon-color, #{$nav-icon-color});");
+ StringAssert.Contains(stylesheet, "$nav-hover-description-color: var(--bit-Nav-hover-description-color, #{$nav-description-color});");
+
+ // Each state reads its own variables, in its own rule: a string searched for in the whole stylesheet would be
+ // found in another state's fallbacks too.
+ var item = SourceFiles.GetScssBlock(stylesheet, "\n.bit-nav-ict {");
+ var hover = SourceFiles.GetScssBlock(item, "&:hover {");
+ StringAssert.Contains(hover, "@include accented-item-content($nav-hover-color, $nav-hover-icon-color, $nav-hover-description-color, $state: true);");
+
+ // On a touch screen no hover ever applies, so the press falls back on the colors at rest, not on a hover's a
+ // theme set for a fill the tap never shows; where the pointer can hover, a press is a hover too, and that rule
+ // comes later to win there.
+ var pressed = SourceFiles.GetScssBlock(item, "\n &:active {");
+ StringAssert.Contains(pressed, "var(--bit-Nav-pressed-color, #{$clr-fg-pri}),");
+ StringAssert.Contains(pressed, "var(--bit-Nav-pressed-icon-color, #{$nav-icon-color}),");
+ StringAssert.Contains(pressed, "var(--bit-Nav-pressed-description-color, #{$nav-description-color}),");
+ Assert.IsFalse(pressed.Contains("hover", StringComparison.Ordinal), "A press on a touch screen falls back on the hovered colors.");
+
+ var hoverablePressAt = item.LastIndexOf("@media (hover: hover) {", StringComparison.Ordinal);
+ Assert.IsTrue(hoverablePressAt > item.IndexOf("\n &:active {", StringComparison.Ordinal), "The press of a pointer that can hover is not declared after the one of every pointer.");
+ var hoverablePress = SourceFiles.GetScssBlock(SourceFiles.GetScssBlock(item[hoverablePressAt..], "@media (hover: hover) {"), "&:active {");
+ StringAssert.Contains(hoverablePress, "var(--bit-Nav-pressed-color, #{$nav-hover-color}),");
+ StringAssert.Contains(hoverablePress, "var(--bit-Nav-pressed-icon-color, #{$nav-hover-icon-color}),");
+ StringAssert.Contains(hoverablePress, "var(--bit-Nav-pressed-description-color, #{$nav-hover-description-color}),");
+
+ var selected = SourceFiles.GetScssBlock(stylesheet, "\n.bit-nav-sel,");
+ StringAssert.Contains(selected, "var(--bit-Nav-selected-color, #{$clr-fg-pri}),");
+ StringAssert.Contains(selected, "var(--bit-Nav-selected-icon-color, #{$nav-icon-color}),");
+ StringAssert.Contains(selected, "var(--bit-Nav-selected-description-color, #{$nav-description-color}));");
+
+ // An explicit Accent and an explicit Color still win over them: the mixin reads its own private variables first.
+ var mixin = SourceFiles.GetScssBlock(stylesheet, "@mixin accented-item-content(");
+ StringAssert.Contains(mixin, "$text: var(--bit-nav-clr-text, #{$color});");
+ StringAssert.Contains(mixin, "$icon: var(--bit-nav-clr-icon, var(--bit-nav-clr, #{$icon-color}));");
+ StringAssert.Contains(mixin, "$des: var(--bit-nav-clr-des, #{$description-color});");
+ }
+
+ [TestMethod]
+ public void BitNavShouldLetEveryAccentWinOverTheStateColorsOfTheIconAndTheDescription()
+ {
+ var accent = SourceFiles.GetScssBlock(ReadStylesheet(), "\n .bit-nav-a#{$role} {");
+
+ // A semantic accent draws them in its on-color. A surface one keeps them in the colors they have at rest on its
+ // tint - but publishes them all the same, so a hovered, pressed or selected variable a theme set for its own
+ // fill never lands on the accent's.
+ StringAssert.Contains(accent, "--bit-nav-clr-icon: #{role($tokens, on)};");
+ StringAssert.Contains(accent, "--bit-nav-clr-des: #{role($tokens, on)};");
+ StringAssert.Contains(accent, "--bit-nav-clr-icon: var(--bit-nav-clr, #{$nav-icon-color});");
+ StringAssert.Contains(accent, "--bit-nav-clr-des: #{$nav-description-color};");
+ }
+
[TestMethod]
public void BitNavShouldPublishItsColorAccentAndSizeOnlyWhereTheyAreSet()
{
diff --git a/src/BlazorUI/Tests/Performance/Bit.BlazorUI.Tests.Performance.TestHost/Components/Pages/BitNavStateColors.razor b/src/BlazorUI/Tests/Performance/Bit.BlazorUI.Tests.Performance.TestHost/Components/Pages/BitNavStateColors.razor
new file mode 100644
index 00000000000..67af177f93d
--- /dev/null
+++ b/src/BlazorUI/Tests/Performance/Bit.BlazorUI.Tests.Performance.TestHost/Components/Pages/BitNavStateColors.razor
@@ -0,0 +1,77 @@
+@page "/regression/nav-state-colors"
+
+
Status: @_status
+