UI: New side nav styles, abstractions (#30568)

## #16846 


[Demo](https://drive.google.com/file/d/1xocZDfOUbu29tPpf2J6dngy3pLACIe62/view?usp=drivesdk)

- [x] Changes file added for user-visible changes in `changes/`
- [x] Manual QA for all new/changed functionality

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Added tooltips to navigation and category menu items for improved
accessibility and clarity.
* Introduced a new optional tooltip position setting, allowing tooltips
to appear on any side of the element.
  * Expanded the color palette with a new light shade option.

* **Style**
* Refactored navigation and category menu styles to use centralized,
reusable mixins for a more consistent appearance.
* Updated navigation and category menu layouts for better structure and
maintainability.

* **Chores**
* Added new SCSS mixins for navigation styling, improving code
maintainability and consistency.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Jacob Shandling <jacob@fleetdm.com>
This commit is contained in:
jacobshandling
2025-07-07 08:29:09 -07:00
committed by GitHub
co-authored by Jacob Shandling
parent dbd6a23053
commit 5f820febdc
12 changed files with 89 additions and 82 deletions
+1
View File
@@ -0,0 +1 @@
* Update side nave styles across the app
@@ -15,6 +15,7 @@ interface ITooltipTruncatedTextCellProps {
* By default the tooltip text breaks on any character. Default: false */
tooltipBreakOnWord?: boolean;
className?: string;
tooltipPosition?: "top" | "bottom" | "left" | "right";
}
const baseClass = "tooltip-truncated-text";
@@ -24,6 +25,7 @@ const TooltipTruncatedText = ({
tooltip,
tooltipBreakOnWord = false,
className,
tooltipPosition = "top",
}: ITooltipTruncatedTextCellProps): JSX.Element => {
const classNames = classnames(baseClass, className, {
"tooltip-break-on-word": tooltipBreakOnWord,
@@ -42,7 +44,7 @@ const TooltipTruncatedText = ({
</div>
</div>
<ReactTooltip
place="top"
place={tooltipPosition}
effect="solid"
backgroundColor={COLORS["tooltip-bg"]}
id={tooltipId}
@@ -38,11 +38,12 @@ describe("Integrations Page", () => {
// <IntegrationsPage router={mockRouter} params={{ section: "mdm" }} />
// );
// sidenav label, sidenav tooltip, and card header
// await waitForLoadingToFinish(container);
// expect(
// screen.getAllByText("Mobile device management (MDM)")
// ).toHaveLength(2);
// ).toHaveLength(3);
// });
// });
describe("Conditional access", () => {
@@ -4,17 +4,7 @@
}
&__nav-list {
position: -webkit-sticky;
position: sticky;
// this is the spacing needed to make the sticky form nav position correctly when scrolling
// TODO: find a way to calculate these sticky positions this and use variables.
// will be tedious to update otherwise.
top: 217px;
width: 178px;
margin: 0;
padding: 0 110px 0 0;
list-style: none;
font-size: $x-small;
@include side-nav-list;
}
&__card-container {
@@ -1,6 +1,7 @@
import React from "react";
import { Link } from "react-router";
import classnames from "classnames";
import TooltipTruncatedText from "components/TooltipTruncatedText";
interface ISideNavItemProps {
title: string;
@@ -11,14 +12,14 @@ interface ISideNavItemProps {
const baseClass = "side-nav-item";
const SideNavItem = ({ title, path, isActive }: ISideNavItemProps) => {
const linkClassnames = classnames(`${baseClass}__nav-link`, {
"active-nav": isActive,
const wrapperClasses = classnames(baseClass, {
[`${baseClass}--active`]: isActive,
});
return (
<li className={baseClass}>
<Link className={linkClassnames} to={path}>
{title}
<li className={wrapperClasses}>
<Link to={path}>
<TooltipTruncatedText value={title} tooltipPosition="right" />
</Link>
</li>
);
@@ -1,19 +1,3 @@
.side-nav-item {
margin-bottom: $pad-medium;
a {
color: $core-fleet-black;
font-weight: $regular;
text-decoration: none;
cursor: pointer;
&:hover {
color: $core-vibrant-blue;
}
&.active-nav {
font-weight: $bold;
}
}
@include side-nav-item;
}
@@ -2,6 +2,8 @@ import React from "react";
import classNames from "classnames";
import LinkWithContext from "components/LinkWithContext";
import TooltipTruncatedText from "components/TooltipTruncatedText";
import { parseHostSoftwareQueryParams } from "../../HostSoftware";
import { ICategory } from "../helpers";
@@ -21,35 +23,37 @@ const CategoriesMenu = ({
const wrapperClasses = classNames(baseClass, className);
return (
<div className={wrapperClasses}>
<ul className={wrapperClasses}>
{categories.map((cat: ICategory) => {
const isActive =
cat.id === queryParams.category_id ||
(cat.id === 0 && !queryParams.category_id);
return (
<LinkWithContext
key={cat.value ?? "all"}
withParams={{
type: "query",
names: ["query", "page", "category_id"],
}}
currentQueryParams={{
...queryParams,
page: 0,
category_id: cat.id !== 0 ? cat.id : undefined,
}}
to={location.pathname}
className={classNames({
[`${baseClass}__category-link`]: true,
<li
className={classNames(`${baseClass}__category-link`, {
[`${baseClass}__category-link--active`]: isActive,
})}
key={cat.value ?? "all"}
>
<span data-text="">{cat.label}</span>
</LinkWithContext>
<LinkWithContext
withParams={{
type: "query",
names: ["query", "page", "category_id"],
}}
currentQueryParams={{
...queryParams,
page: 0,
category_id: cat.id !== 0 ? cat.id : undefined,
}}
to={location.pathname}
>
<TooltipTruncatedText value={cat.label} tooltipPosition="right" />
</LinkWithContext>
</li>
);
})}
</div>
</ul>
);
};
export default CategoriesMenu;
@@ -1,31 +1,6 @@
.categories-menu {
display: flex;
flex-direction: column;
width: 232px;
// Matches styling in DropdownWrapper > options
@include side-nav-list;
&__category-link {
padding: 10px 8px;
font-size: $x-small;
font-weight: $regular;
border-radius: $border-radius;
color: $core-fleet-black;
// current selection
&--active {
font-weight: $bold;
}
}
// Mouse over/mouse click states
&:hover {
.categories-menu__category-link:hover {
background-color: $ui-vibrant-blue-10;
cursor: "pointer";
}
.categories-menu__category-link:active {
background-color: $ui-vibrant-blue-25;
}
@include side-nav-item;
}
}
@@ -61,8 +61,6 @@
@media (min-width: $break-md) {
.categories-menu {
display: flex;
flex-direction: column;
width: 300px;
}
&__categories-dropdown {
+1
View File
@@ -16,6 +16,7 @@ $ui-fleet-black-33: #b3b6c1;
$ui-fleet-black-25: #c5c7d1;
$ui-fleet-black-10: #e2e4ea;
$ui-fleet-blue-10: #f9fafc;
$ui-fleet-black-5: #f4f4f6;
$ui-dark-blue-gray: #afbec1;
$ui-blue-gray: #dbe3e5;
$ui-gray: #e3e3e3;
+1
View File
@@ -14,6 +14,7 @@ export const COLORS = {
"ui-fleet-black-33": "#B3B6C1",
"ui-fleet-black-25": "#C5C7D1",
"ui-fleet-black-10": "#E2E4EA",
"ui-fleet-black-5": "#F4F4F6",
"ui-off-white": "#F9FAFC",
"ui-blue-hover": "#5D5AE7",
"ui-blue-pressed": "#4B4AB4",
+49
View File
@@ -401,3 +401,52 @@ $max-width: 2560px;
transform: rotate(315deg);
}
}
@mixin side-nav-list {
position: -webkit-sticky;
position: sticky;
// this is the spacing needed to make the sticky form nav position correctly when scrolling
// TODO: find a way to calculate these sticky positions this and use variables.
// will be tedious to update otherwise.
top: 217px;
width: 260px;
padding: 0 64px 0 0;
list-style: none;
font-size: $x-small;
display: flex;
flex-direction: column;
gap: $pad-small;
margin: 0;
}
@mixin side-nav-item {
white-space: nowrap;
height: 32px;
border-radius: 4px;
a {
display: flex;
align-items: center;
height: 100%;
padding: 0 16px;
color: $core-fleet-black;
font-weight: $regular;
text-decoration: none;
cursor: pointer;
&:hover {
color: $core-vibrant-blue;
}
}
&--active {
background-color: $ui-fleet-black-5;
a {
font-weight: $bold;
color: $ui-fleet-black-75;
* > .__react_component_tooltip {
font-weight: initial;
}
}
}
}