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
206 changes: 206 additions & 0 deletions src/components/FrameworkSelectorMobile/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
import React, { useEffect, useRef, useState } from "react";
import Link from "@docusaurus/Link";
import { useLocation } from "@docusaurus/router";
import {
useVersions,
useActiveDocContext,
} from "@docusaurus/plugin-content-docs/client";
import { useDocsPreferredVersion } from "@docusaurus/theme-common";
import { useFrameworkItems } from "@site/src/utils/useFrameworkItems";
import { frameworkCards } from "@site/src/components/HomePage/data/frameworkCardsArr";
import { FrameworksName } from "@site/src/components/constants/frameworksName";
import { ArrowDown } from "@site/src/components/IconComponents";
import styles from "./styles.module.css";

// Reuse the homepage framework icons, keyed by display label ("iOS", ".NET Android", ...)
const iconsByLabel: Record<string, React.ReactNode> = {};
frameworkCards.forEach((card) => {
iconsByLabel[FrameworksName[card.framework as keyof typeof FrameworksName]] =
card.icon;
card.additional?.forEach((additionalCard) => {
iconsByLabel[
FrameworksName[additionalCard.framework as keyof typeof FrameworksName]
] = additionalCard.icon;
});
});

// Same categorization as the navbar version dropdown
const VERSION_CATEGORY: Record<string, string> = {
current: "stable",
};
const TAG_LABEL: Record<string, string> = {
stable: "Stable",
beta: "Beta",
legacy: "Legacy",
};

const getVersionMainDoc = (version) =>
version.docs.find((doc) => doc.id === version.mainDocId);

function useDropdown(close: () => void) {
const fieldRef = useRef<HTMLDivElement>(null);
const menuRef = useRef<HTMLDivElement>(null);

useEffect(() => {
function handleClickOutside(event: MouseEvent | TouchEvent) {
if (
menuRef.current &&
!menuRef.current.contains(event.target as Node) &&
fieldRef.current &&
!fieldRef.current.contains(event.target as Node)
) {
close();
}
}
document.addEventListener("mousedown", handleClickOutside);
document.addEventListener("touchstart", handleClickOutside);
return () => {
document.removeEventListener("mousedown", handleClickOutside);
document.removeEventListener("touchstart", handleClickOutside);
};
}, [close]);

return { fieldRef, menuRef };
}

/**
* Homepage-style framework and SDK version selector shown at the top of doc
* pages on mobile, where the desktop navbar dropdowns are not available.
* Switching keeps the current page (and version, for framework switches).
*/
export default function FrameworkSelectorMobile() {
const { currentFramework, frameworkItems } = useFrameworkItems();
const { search, hash, pathname } = useLocation();
const activeDocContext = useActiveDocContext(undefined);
const versions = useVersions(undefined);
const { savePreferredVersionName } = useDocsPreferredVersion();

const [openSelector, setOpenSelector] = useState<
"framework" | "version" | null
>(null);
const framework = useDropdown(() =>
setOpenSelector((open) => (open === "framework" ? null : open))
);
const version = useDropdown(() =>
setOpenSelector((open) => (open === "version" ? null : open))
);

if (!currentFramework) {
return null;
}

// Xamarin only exists in the legacy versions
const filteredVersions = pathname.includes("/xamarin/")
? versions.filter((v) => v.name === "7.6.14" || v.name === "6.28.10")
: versions;
const activeVersion = activeDocContext?.activeVersion;

const versionLinks = filteredVersions.map((v) => {
// Prefer the equivalent of the current page in version v; fall back to that
// version's main doc, and finally to "/" if neither resolves.
const versionDoc =
activeDocContext?.alternateDocVersions[v.name] ?? getVersionMainDoc(v);
return {
name: v.name,
label: v.label,
category: VERSION_CATEGORY[v.name] || "legacy",
to: `${versionDoc?.path ?? "/"}${search}${hash}`,
// v (from useVersions) and activeVersion (from useActiveDocContext) come
// from different hooks, so compare by name rather than reference.
isActive: v.name === activeVersion?.name,
};
});

const versionTag = (category: string) => (

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getVersionMainDoc can return undefined here if a version has no doc matching mainDocId, and .path would throw. The ?? only guards the first operand — maybe (... ?? getVersionMainDoc(v))?.path ?? "/".

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch — fixed in 0e90280. Resolve versionDoc once and use versionDoc?.path ?? "/" so a version with no matching main doc falls back to / instead of throwing.

<span className={`version-tag version-tag-${category}`}>
{TAG_LABEL[category]}
</span>
);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

v comes from useVersions() and activeVersion from useActiveDocContext() — different hooks, so reference equality may never hold and the active version highlight won't show. Compare v.name === activeVersion?.name instead.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, reference equality never held — the active highlight was silently always off. Switched to v.name === activeVersion?.name in 0e90280. Verified at runtime: 8.4.0 option is now active on 8.4.0 pages, 7.6.14 on 7.6.14 pages.


return (
<div className={styles.selectorMobile}>
<div className={styles.fieldsRow}>
<div
className={`${styles.field} ${styles.versionField}`}
role="button"
aria-haspopup="listbox"
aria-expanded={openSelector === "version"}
ref={version.fieldRef}
onClick={() =>
setOpenSelector(openSelector === "version" ? null : "version")
}
>
<span className={styles.fieldLabel}>{activeVersion?.label}</span>
<span
className={openSelector === "version" ? styles.open : styles.fieldIcon}
>
<ArrowDown />
</span>
</div>
<div
className={styles.field}
role="button"
aria-haspopup="listbox"
aria-expanded={openSelector === "framework"}
ref={framework.fieldRef}
onClick={() =>
setOpenSelector(openSelector === "framework" ? null : "framework")
}
>
<span className={styles.fieldLabel}>
<span className={styles.optionsIcon}>
{iconsByLabel[currentFramework]}
</span>
{currentFramework}
</span>
<span
className={
openSelector === "framework" ? styles.open : styles.fieldIcon
}
>
<ArrowDown />
</span>
</div>
</div>
{openSelector === "version" && (
<div className={styles.optionsMain} ref={version.menuRef}>
{versionLinks.map((v) => (
<Link
key={v.name}
to={v.to}
className={`${styles.option} ${
v.isActive ? styles.checkedFramework : ""
}`}
onClick={() => {
savePreferredVersionName(v.name);
setOpenSelector(null);
}}
>
<span>{v.label}</span>
{versionTag(v.category)}
</Link>
))}
</div>
)}
{openSelector === "framework" && (
<div className={styles.optionsMain} ref={framework.menuRef}>
{frameworkItems.map((item) => (
<Link
key={item.label}
to={item.to}
className={`${styles.option} ${
item.label === currentFramework ? styles.checkedFramework : ""
}`}
onClick={() => setOpenSelector(null)}
>
<span className={styles.optionsIcon}>
{iconsByLabel[item.label]}
</span>
<span>{item.label}</span>
</Link>
))}
</div>
)}
</div>
);
}
119 changes: 119 additions & 0 deletions src/components/FrameworkSelectorMobile/styles.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
.selectorMobile {
position: relative;
margin-bottom: 20px;
/* Full width on phones, but don't stretch across tablet-width viewports
(Docusaurus treats everything under 997px as mobile) */
max-width: 420px;
}

/* From tablet width up, the navbar shows the version + framework dropdowns
(see navbar.scss), so the in-page selector is only for narrow screens */
@media (min-width: 768px) {
.selectorMobile {
display: none;
}
}

.fieldsRow {
display: flex;
gap: 8px;
}

.field {
width: 100%;
border-radius: 100px;
border: 1px solid var(--nav-border);
height: 48px;
display: flex;
align-items: center;
justify-content: space-between;
padding: 6px 20px;
color: var(--light-black);
font-size: 16px;
font-weight: 400;
cursor: pointer;
}

/* The version pill only needs to fit its short label.
Declared after .field so its width override wins. */
.versionField {
flex: 0 0 auto;
width: auto;
gap: 10px;
}

.fieldLabel {
display: flex;
align-items: center;
gap: 10px;
}

.fieldIcon,
.open {
height: 20px;
}

.open {
transform: rotate(180deg);
transition: transform 0.3s ease;
}

.optionsMain {
margin-top: 8px;
box-shadow: 0 0 6px #10233223;
border-radius: 10px;
padding: 16px 20px;
position: absolute;
background-color: var(--frameworksBgLight);
width: 100%;
max-height: 440px;
overflow-y: auto;
top: 48px;
/* Above in-page content like the SkillsCallout, whose summary/body are z-index 1 */
z-index: 20;
}

.optionsMain::-webkit-scrollbar {
width: 8px;
}

.optionsMain::-webkit-scrollbar-thumb {
background-color: #888;
border-radius: 10px;
}

.optionsMain::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 10px;
}

.option {
display: flex;
align-items: center;
gap: 8px;
padding: 10px;
cursor: pointer;
border-radius: 8px;
color: var(--light-black);
}

.option:hover {
background-color: var(--frameworksBgHover);
text-decoration: none;
color: var(--light-black);
}

.optionsIcon {
display: flex;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
}

/* Same selected style as the homepage framework selector */
.checkedFramework,
.checkedFramework:hover {
background-color: #121619;
color: #fff;
}
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,8 @@
max-height: 440px;
overflow-y: auto;
top: 50px;
z-index: 1;
/* Above in-page content like the SkillsCallout, whose summary/body are z-index 1 */
z-index: 20;
}

/* Styles for the scrollbar */
Expand Down
10 changes: 10 additions & 0 deletions src/css/components/doc-content.scss
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,16 @@
#__docusaurus [class*="docMainContainer"] {
max-width: 100%;
overflow: hidden;

& .container {
padding-top: 16px !important;
}
}

/* On mobile the breadcrumbs make way for the framework selector; the
sidebar menu still highlights the open page for orientation. */
#__docusaurus .theme-doc-breadcrumbs {
display: none;
}
}

Expand Down
6 changes: 6 additions & 0 deletions src/css/components/navbar-sidebar.scss
Original file line number Diff line number Diff line change
Expand Up @@ -91,3 +91,9 @@
html[data-theme="dark"] .navbar-sidebar {
background-color: var(--dark-blue-90);
}

/* The in-page framework/version selector replaced the primary-menu detour
on doc pages, so the "Back to main menu" button only adds noise. */
#__docusaurus .navbar-sidebar__back {
display: none;
}
11 changes: 11 additions & 0 deletions src/css/components/navbar.scss
Original file line number Diff line number Diff line change
Expand Up @@ -307,6 +307,17 @@ html[data-theme="dark"] .navbar {
}
}

/* On tablets there is room in the navbar for the version + framework
dropdowns (Infima hides all navbar items below 997px); the in-page
selector takes over only below 768px. */
@media screen and (min-width: 768px) and (max-width: 996px) {
.navbar {
& .navbar__items:not(.navbar__items--right) .navbar__item.dropdown {
display: block;
}
}
}

@media screen and (max-width: 1120px) {
.navbar {
& .navbar__title {
Expand Down
Loading
Loading