Skip to content

Commit

Permalink
fix: improve sidebar ux (#1410)
Browse files Browse the repository at this point in the history
  • Loading branch information
abvthecity authored Sep 9, 2024
1 parent 1986fca commit afa022a
Show file tree
Hide file tree
Showing 28 changed files with 819 additions and 556 deletions.
1 change: 1 addition & 0 deletions packages/ui/app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@
"@next/third-parties": "^14.2.4",
"@radix-ui/colors": "^3.0.0",
"@radix-ui/react-accordion": "^1.1.2",
"@radix-ui/react-collapsible": "^1.1.0",
"@radix-ui/react-dialog": "^1.1.1",
"@radix-ui/react-popover": "^1.1.1",
"@radix-ui/react-select": "^2.0.0",
Expand Down
27 changes: 19 additions & 8 deletions packages/ui/app/src/atoms/layout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,21 +14,28 @@ export const DOCS_LAYOUT_ATOM = selectAtom(
);
DOCS_LAYOUT_ATOM.debugLabel = "DOCS_LAYOUT_ATOM";

function getHeaderHeightPx(layout: DocsV1Read.DocsLayoutConfig | undefined): number {
if (layout?.headerHeight?.type === "px") {
return layout.headerHeight.value;
} else if (layout?.headerHeight?.type === "rem") {
return layout.headerHeight.value * 16;
} else {
return 64;
}
}

export const HEADER_HEIGHT_ATOM = atom<number>((get) => {
const layout = get(DOCS_LAYOUT_ATOM);
const isMobileSidebarEnabled = get(MOBILE_SIDEBAR_ENABLED_ATOM);
const headerHeight =
layout?.headerHeight == null
? 64
: layout.headerHeight.type === "px"
? layout.headerHeight.value
: layout.headerHeight.type === "rem"
? layout.headerHeight.value * 16
: 64;
const headerHeight = getHeaderHeightPx(layout);
return isMobileSidebarEnabled || layout?.disableHeader !== true ? headerHeight : 0;
});
HEADER_HEIGHT_ATOM.debugLabel = "HEADER_HEIGHT_ATOM";

export const MOBILE_HEADER_HEIGHT_ATOM = atom<number>((get) => {
return getHeaderHeightPx(get(DOCS_LAYOUT_ATOM));
});

const SETTABLE_HEADER_TABS_HEIGHT_ATOM = atom<number>(44);
SETTABLE_HEADER_TABS_HEIGHT_ATOM.debugLabel = "SETTABLE_HEADER_TABS_HEIGHT_ATOM";

Expand Down Expand Up @@ -87,6 +94,10 @@ export const HEADER_OFFSET_ATOM = atom<number>((get) => {
});
HEADER_OFFSET_ATOM.debugLabel = "HEADER_OFFSET_ATOM";

export const MOBILE_HEADER_OFFSET_ATOM = atom<number>((get) => {
return get(MOBILE_HEADER_HEIGHT_ATOM) + get(ANNOUNCEMENT_HEIGHT_ATOM);
});

export const BELOW_HEADER_HEIGHT_ATOM = atom<number>((get) => {
const headerHeight = get(HEADER_HEIGHT_ATOM);
const windowHeight = get(VIEWPORT_HEIGHT_ATOM);
Expand Down
193 changes: 124 additions & 69 deletions packages/ui/app/src/atoms/sidebar.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,18 @@
import * as FernNavigation from "@fern-api/fdr-sdk/navigation";
import { SetStateAction, atom, useAtomValue, useSetAtom } from "jotai";
import { useAtomCallback } from "jotai/utils";
import { atom, useAtomValue, useSetAtom } from "jotai";
import { RESET, atomWithDefault, useAtomCallback } from "jotai/utils";
import { useCallback } from "react";
import { useCallbackOne, useMemoOne } from "use-memo-one";
import { FEATURE_FLAGS_ATOM } from "./flags";
import { useAtomEffect } from "./hooks";
import { DOCS_LAYOUT_ATOM } from "./layout";
import { CURRENT_NODE_ATOM, CURRENT_NODE_ID_ATOM, RESOLVED_PATH_ATOM, SIDEBAR_ROOT_NODE_ATOM } from "./navigation";
import {
CURRENT_NODE_ATOM,
CURRENT_NODE_ID_ATOM,
NAVIGATION_NODES_ATOM,
RESOLVED_PATH_ATOM,
SIDEBAR_ROOT_NODE_ATOM,
} from "./navigation";
import { THEME_ATOM } from "./theme";
import { IS_MOBILE_SCREEN_ATOM, MOBILE_SIDEBAR_ENABLED_ATOM } from "./viewport";

Expand Down Expand Up @@ -42,24 +49,22 @@ export const SIDEBAR_PARENT_TO_CHILDREN_MAP_ATOM = atom((get) => {
});
SIDEBAR_PARENT_TO_CHILDREN_MAP_ATOM.debugLabel = "SIDEBAR_PARENT_TO_CHILDREN_MAP_ATOM";

const INTERNAL_EXPANDED_SIDEBAR_NODES_ATOM = atom<{
const INTERNAL_EXPANDED_SIDEBAR_NODES_ATOM = atomWithDefault<{
sidebarRootId: FernNavigation.NodeId;
expandedNodes: FernNavigation.NodeId[];
}>({
sidebarRootId: FernNavigation.NodeId(""),
expandedNodes: [],
});

const INITIAL_EXPANDED_SIDEBAR_NODES_ATOM = atom((get) => {
expandedNodes: Set<FernNavigation.NodeId>;
implicitExpandedNodes: Set<FernNavigation.NodeId>;
}>((get) => {
const sidebar = get(SIDEBAR_ROOT_NODE_ATOM);
const expandedNodes = new Set<FernNavigation.NodeId>();
const childToParentsMap = get(SIDEBAR_CHILD_TO_PARENTS_MAP_ATOM);
const currentNode = get(CURRENT_NODE_ATOM);
const currentNode = get(CURRENT_NODE_ID_ATOM);
if (currentNode != null) {
expandedNodes.add(currentNode.id);
childToParentsMap.get(currentNode.id)?.forEach((parent) => {
expandedNodes.add(currentNode);
childToParentsMap.get(currentNode)?.forEach((parent) => {
expandedNodes.add(parent);
});
}

// TODO: compute default expanded nodes
// the following was commented out because FDR stores `collapsed: false` by default. Another solution is needed.
// const sidebar = get(SIDEBAR_ROOT_NODE_ATOM);
Expand All @@ -71,64 +76,61 @@ const INITIAL_EXPANDED_SIDEBAR_NODES_ATOM = atom((get) => {
// }
// });
// }
return [...expandedNodes];

return {
sidebarRootId: sidebar?.id ?? FernNavigation.NodeId(""),
expandedNodes: new Set(),
implicitExpandedNodes: expandedNodes,
};
});
INITIAL_EXPANDED_SIDEBAR_NODES_ATOM.debugLabel = "INITIAL_EXPANDED_SIDEBAR_NODES_ATOM";

export const EXPANDED_SIDEBAR_NODES_ATOM = atom(
(get) => {
const sidebar = get(SIDEBAR_ROOT_NODE_ATOM);
const { expandedNodes: internalExpandedNodes, sidebarRootId } = get(INTERNAL_EXPANDED_SIDEBAR_NODES_ATOM);
export function useInitSidebarExpandedNodes(): void {
useAtomEffect(
useCallbackOne((get, set) => {
const currentNodeId = get(CURRENT_NODE_ID_ATOM);

// when the sidebar changes, reset the expanded nodes to the initial state
if (sidebarRootId !== sidebar?.id) {
return new Set(get(INITIAL_EXPANDED_SIDEBAR_NODES_ATOM));
}
if (currentNodeId == null) {
return;
}

const childToParentsMap = get(SIDEBAR_CHILD_TO_PARENTS_MAP_ATOM);
const expandedNodes = new Set<FernNavigation.NodeId>();
internalExpandedNodes.forEach((nodeId) => {
expandedNodes.add(nodeId);
childToParentsMap.get(nodeId)?.forEach((parent) => {
expandedNodes.add(parent);
});
});
const current = get(CURRENT_NODE_ATOM);
if (current != null) {
expandedNodes.add(current.id);
childToParentsMap.get(current.id)?.forEach((parent) => {
expandedNodes.add(parent);
});
}
return expandedNodes;
},
(get, set, update: SetStateAction<FernNavigation.NodeId[]>) => {
const sidebar = get(SIDEBAR_ROOT_NODE_ATOM);
const internal = get(INTERNAL_EXPANDED_SIDEBAR_NODES_ATOM);

// when the sidebar changes, reset the expanded nodes to the initial state
if (internal.sidebarRootId !== sidebar?.id) {
const expandedNodes = get(INITIAL_EXPANDED_SIDEBAR_NODES_ATOM);
set(INTERNAL_EXPANDED_SIDEBAR_NODES_ATOM, {
sidebarRootId: sidebar?.id ?? FernNavigation.NodeId(""),
expandedNodes: typeof update === "function" ? update(expandedNodes) : update,
const sidebarNodeId = get.peek(SIDEBAR_ROOT_NODE_ATOM)?.id;

// resets the sidebar expanded state when switching between tabs or versions
if (sidebarNodeId !== get.peek(INTERNAL_EXPANDED_SIDEBAR_NODES_ATOM).sidebarRootId) {
set(INTERNAL_EXPANDED_SIDEBAR_NODES_ATOM, RESET);
return;
}

set(INTERNAL_EXPANDED_SIDEBAR_NODES_ATOM, (prev) => {
const childToParentsMap = get.peek(SIDEBAR_CHILD_TO_PARENTS_MAP_ATOM);

// always clear the implicitly expanded nodes as a side effect of changing the current node
const implicitExpandedNodes = new Set<FernNavigation.NodeId>();
implicitExpandedNodes.add(currentNodeId);
childToParentsMap.get(currentNodeId)?.forEach((parent) => {
implicitExpandedNodes.add(parent);
});
return {
sidebarRootId: prev.sidebarRootId,
expandedNodes: prev.expandedNodes,
implicitExpandedNodes,
};
});
} else {
// only update the expanded nodes that are still in the sidebar
const nodes = get(SIDEBAR_CHILD_TO_PARENTS_MAP_ATOM);
set(INTERNAL_EXPANDED_SIDEBAR_NODES_ATOM, (prev) => ({
sidebarRootId: prev.sidebarRootId,
expandedNodes: (typeof update === "function" ? update(prev.expandedNodes) : update).filter((nodeId) =>
nodes.has(nodeId),
),
}));
}
},
);
EXPANDED_SIDEBAR_NODES_ATOM.debugLabel = "EXPANDED_SIDEBAR_NODES_ATOM";
}, []),
);
}

export const useIsExpandedSidebarNode = (nodeId: FernNavigation.NodeId): boolean => {
return useAtomValue(useMemoOne(() => atom((get) => get(EXPANDED_SIDEBAR_NODES_ATOM).has(nodeId)), [nodeId]));
return useAtomValue(
useMemoOne(
() =>
atom((get) => {
const { expandedNodes, implicitExpandedNodes } = get(INTERNAL_EXPANDED_SIDEBAR_NODES_ATOM);
return expandedNodes.has(nodeId) || implicitExpandedNodes.has(nodeId);
}),
[nodeId],
),
);
};

export const useIsSelectedSidebarNode = (nodeId: FernNavigation.NodeId): boolean => {
Expand Down Expand Up @@ -160,12 +162,65 @@ export const useToggleExpandedSidebarNode = (nodeId: FernNavigation.NodeId): (()
useCallbackOne(
(get, set) => {
const parentToChildrenMap = get(SIDEBAR_PARENT_TO_CHILDREN_MAP_ATOM);
set(EXPANDED_SIDEBAR_NODES_ATOM, (prev) => {
if (prev.includes(nodeId)) {
const childToParentsMap = get(SIDEBAR_CHILD_TO_PARENTS_MAP_ATOM);

set(INTERNAL_EXPANDED_SIDEBAR_NODES_ATOM, (prev) => {
const expandedNodes = new Set(prev.expandedNodes);
const implicitExpandedNodes = new Set(prev.implicitExpandedNodes);
const collector = get(NAVIGATION_NODES_ATOM);

if (prev.expandedNodes.has(nodeId) || prev.implicitExpandedNodes.has(nodeId)) {
const node = collector.get(nodeId);

if (node != null && node.id !== get(CURRENT_NODE_ID_ATOM) && FernNavigation.hasMarkdown(node)) {
return prev;
}

// remove this node and all children from the expanded set
return prev.filter((id) => id !== nodeId && !parentToChildrenMap.get(nodeId)?.includes(id));
// return prev.filter((id) => id !== nodeId && !parentToChildrenMap.get(nodeId)?.includes(id));
expandedNodes.delete(nodeId);
implicitExpandedNodes.delete(nodeId);
parentToChildrenMap.get(nodeId)?.forEach((child) => {
expandedNodes.delete(child);
implicitExpandedNodes.delete(child);
});
return {
sidebarRootId: prev.sidebarRootId,
expandedNodes,
implicitExpandedNodes,
};
} else {
return [...prev, nodeId];
const parents = childToParentsMap.get(nodeId) ?? [];
const { isApiScrollingDisabled } = get(FEATURE_FLAGS_ATOM);

// if long scrolling is enabled, implicitly "expand" its parent nodes
if (!isApiScrollingDisabled) {
const isLongScrollingApiReference = [...parents, nodeId]
.map((id) => collector.get(id))
.some((node) => node?.type === "apiReference" && !node.paginated);
if (isLongScrollingApiReference) {
implicitExpandedNodes.add(nodeId);
parents.forEach((parent) => {
implicitExpandedNodes.add(parent);
});
return {
sidebarRootId: prev.sidebarRootId,
expandedNodes,
implicitExpandedNodes,
};
}
}

expandedNodes.add(nodeId);
childToParentsMap.get(nodeId)?.forEach((child) => {
expandedNodes.add(child);
});

return {
sidebarRootId: prev.sidebarRootId,
expandedNodes,
implicitExpandedNodes,
};
}
});
},
Expand Down
17 changes: 5 additions & 12 deletions packages/ui/app/src/components/HttpMethodTag.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,7 @@ const METHOD_COLOR_SCHEMES: Record<HttpMethodTag.Props["method"], FernTagColorSc
PATCH: "amber",
};

const UnmemoizedHttpMethodTag: React.FC<HttpMethodTag.Props> = ({
method,
active,
size = "lg",
className,
...rest
}) => {
export const HttpMethodTag = memo<HttpMethodTag.Props>(({ method, active, size = "lg", className, ...rest }) => {
return (
<FernTag
colorScheme={METHOD_COLOR_SCHEMES[method]}
Expand All @@ -45,13 +39,14 @@ const UnmemoizedHttpMethodTag: React.FC<HttpMethodTag.Props> = ({
{method === APIV1Read.HttpMethod.Delete ? "DEL" : method}
</FernTag>
);
};
});
HttpMethodTag.displayName = "HttpMethodTag";

export function withStream(text: ReactNode, size: "sm" | "lg" = "sm"): ReactNode {
return (
<span className="inline-flex items-center gap-2">
<span>{text}</span>
<UnmemoizedHttpMethodTag size={size} method="STREAM" />
<HttpMethodTag size={size} method="STREAM" />
</span>
);
}
Expand All @@ -60,10 +55,8 @@ export function withWss(text: ReactNode, size: "sm" | "lg" = "sm"): ReactNode {
<span className="inline-flex items-center gap-2">
<span>{text}</span>
<FernTooltip content="WebSocket Channel">
<UnmemoizedHttpMethodTag size={size} method="WSS" />
<HttpMethodTag size={size} method="WSS" />
</FernTooltip>
</span>
);
}

export const HttpMethodTag = memo(UnmemoizedHttpMethodTag);
2 changes: 1 addition & 1 deletion packages/ui/app/src/components/TableOfContents.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ function TableOfContentsItem({
{text.length > 0 && (
<FernLink
className={cn(
"hover:t-default block hyphens-auto break-words py-1.5 text-sm leading-5 transition-all",
"hover:t-default block hyphens-auto break-words py-1.5 text-sm leading-5 transition-all hover:transition-none",
{
"t-muted": anchorInView !== anchorString,
"t-accent-aaa": anchorInView === anchorString,
Expand Down
Loading

0 comments on commit afa022a

Please sign in to comment.