Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
91e5b2e
docs: split apis & types into flat per-symbol pages with platform fol…
hyochan Apr 25, 2026
01b6f86
docs(review): address Copilot + CodeRabbit feedback on PR #106
hyochan Apr 25, 2026
0f66b35
docs(review): round 2 — Copilot follow-up nits
hyochan Apr 25, 2026
58f23b5
docs(review): round 3 — CodeRabbit nits on PR #106
hyochan Apr 25, 2026
9456ad0
docs: add Getting Started landing page and Framework Setup overview
hyochan Apr 25, 2026
b1787a0
docs: restore sidebar collapse animation, scrollable tables, KMP tab
hyochan Apr 25, 2026
9180269
docs: animate sidebar dropdown collapse with rAF + reflow
hyochan Apr 25, 2026
252d4ad
docs: surface Request APIs callout on fetch-products and Ecosystem/Li…
hyochan Apr 25, 2026
9598d9c
docs: explain how the doc site is organized + run prettier
hyochan Apr 25, 2026
8e64d3b
docs(review): address Copilot + CodeRabbit round 3 + ellipsis sidebar…
hyochan Apr 25, 2026
f4b915d
docs(review): split RenewalInfoIOS into its own type page
hyochan Apr 25, 2026
e14e691
docs: split Events into flat per-listener pages, smooth sidebar colla…
hyochan Apr 25, 2026
9f5b10c
docs(review): address Gemini round 4 — legacy anchors, naming, deprec…
hyochan Apr 25, 2026
753bfd5
docs(review): address CodeRabbit round 5 — request* framing, signatur…
hyochan Apr 25, 2026
32672e5
docs(review): preserve verify-purchase anchor granularity in legacy r…
hyochan Apr 25, 2026
4b1ec07
docs: link bare type and field references throughout the docs
hyochan Apr 25, 2026
05120bd
docs(review): preserve hash on legacy redirects + complete legacy anc…
hyochan Apr 25, 2026
a434a74
docs(review): align finishTransaction TS shape, deduplicate AppTransa…
hyochan Apr 25, 2026
64d4d99
docs: weave useIAP, react-native-iap import comment, Flutter/KMP DSL …
hyochan Apr 25, 2026
c79e7e0
docs(review): guard legacy anchor redirects against same-page loops +…
hyochan Apr 25, 2026
b52e7c5
docs(audit): close schema-parity gaps and fix Swift requestPurchase r…
hyochan Apr 25, 2026
3187e8c
docs(audit): close remaining audit gaps
hyochan Apr 25, 2026
b851cd0
docs(review): MenuDropdown title a11y, KMP request-purchase return, G…
hyochan Apr 25, 2026
527bedd
docs(review): finishTransaction Swift, Horizon caveat, hash-preservin…
hyochan Apr 25, 2026
ab85275
docs(review): collapse SubMenu header into single disclosure button (…
hyochan Apr 25, 2026
6aaa6cd
docs(review): close Copilot Round 11 findings
hyochan Apr 25, 2026
91a9db6
docs: add Apple/Google/Horizon native reference links across types & …
hyochan Apr 25, 2026
c32e95e
docs: fix table layout regressions on external-purchase and billing-i…
hyochan Apr 25, 2026
3922e3d
docs(review): fix Onside → Onsite typo (Copilot/Gemini)
hyochan Apr 25, 2026
10af2cd
docs: revert false-positive Onside typo fix
hyochan Apr 25, 2026
b97d672
docs(review): close /review-pr findings (search index, nested links, …
hyochan Apr 25, 2026
5f7d9d3
docs(review): close Round 12 findings (Copilot/Gemini)
hyochan Apr 25, 2026
6bfee7c
docs(review): use object-arg finishTransaction in purchase.tsx TS sam…
hyochan Apr 25, 2026
12d5d53
docs: revert MenuDropdown title-as-NavLink + inert (visual regression)
hyochan Apr 25, 2026
bbf238f
docs(review): close Round 13 findings (Copilot/Gemini/CodeRabbit)
hyochan Apr 25, 2026
801e983
docs(review): close Round 14 findings (Copilot/Gemini)
hyochan Apr 25, 2026
3df44b3
docs(review): close Round 15 findings (Copilot/Gemini)
hyochan Apr 25, 2026
43c7258
docs(review): close Round 16 findings (Copilot/Gemini)
hyochan Apr 25, 2026
c729643
docs(review): close Round 18 findings (CodeRabbit)
hyochan Apr 25, 2026
b64b2d4
docs(review): clarify acknowledgePurchaseAndroid deprecation note (Ge…
hyochan Apr 25, 2026
49e8494
docs(review): differentiate the duplicate /docs/errors links on setup…
hyochan Apr 25, 2026
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
211 changes: 165 additions & 46 deletions packages/docs/src/components/MenuDropdown.tsx
Original file line number Diff line number Diff line change
@@ -1,109 +1,228 @@
import { useState, useRef, useEffect } from 'react';
import { useId, useState, useEffect } from 'react';
import { NavLink, useLocation, useNavigate } from 'react-router-dom';

interface MenuItem {
export interface MenuItem {
to: string;
label: string;
}

export interface MenuGroup {
label: string;
items: MenuItem[];
}

export type MenuEntry = MenuItem | MenuGroup;

function isGroup(entry: MenuEntry): entry is MenuGroup {
return 'items' in entry && Array.isArray((entry as MenuGroup).items);
}

interface MenuDropdownProps {
title: string;
titleTo: string;
items: MenuItem[];
items: MenuEntry[];
onItemClick?: () => void;
}

interface SubMenuProps {
group: MenuGroup;
onItemClick?: () => void;
parentExpanded: boolean;
}

function Chevron({ isExpanded }: { isExpanded: boolean }) {
return (
<svg
width="10"
height="10"
viewBox="0 0 10 10"
aria-hidden="true"
style={{
transform: isExpanded ? 'rotate(90deg)' : 'rotate(0deg)',
transition: 'transform 0.2s ease',
flexShrink: 0,
}}
>
<path
d="M3 1 L7 5 L3 9"
stroke="currentColor"
strokeWidth="1.5"
fill="none"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
}

function SubMenu({ group, onItemClick, parentExpanded }: SubMenuProps) {
const [isExpanded, setIsExpanded] = useState(false);
const location = useLocation();
const submenuContentId = useId();
// NavLinks should leave the tab order whenever this submenu OR its
// parent dropdown is collapsed — otherwise keyboard users can tab into
// hidden links.
const navLinkTabIndex = parentExpanded && isExpanded ? 0 : -1;

const isAnyChildActive = group.items.some(
(item) => location.pathname === item.to
);

useEffect(() => {
if (isAnyChildActive) {
setIsExpanded(true);
}
}, [isAnyChildActive]);

const toggleExpanded = () => setIsExpanded((v) => !v);

return (
<li className="menu-dropdown menu-dropdown--nested">
{/* Single disclosure button — both halves do the same thing, so
collapsing into one element keeps the keyboard tab order at one
stop per group instead of two redundant ones. */}
<button
type="button"
onClick={toggleExpanded}
tabIndex={parentExpanded ? 0 : -1}
className={`menu-dropdown-header ${isAnyChildActive ? 'group-active' : ''}`}
aria-expanded={isExpanded}
aria-controls={submenuContentId}
aria-label={`Toggle ${group.label} submenu`}
>
Comment thread
hyochan marked this conversation as resolved.
<span className="menu-dropdown-title menu-dropdown-title--nested">
{group.label}
</span>
<span className="menu-dropdown-toggle" aria-hidden="true">
<Chevron isExpanded={isExpanded} />
</span>
</button>
<div
id={submenuContentId}
className="menu-dropdown-content"
data-expanded={isExpanded}
Comment thread
hyochan marked this conversation as resolved.
aria-hidden={!isExpanded}
Comment thread
hyochan marked this conversation as resolved.
>
<ul className="menu-dropdown-items menu-dropdown-items--nested">
{group.items.map((item) => (
<li key={item.to}>
<NavLink
to={item.to}
tabIndex={navLinkTabIndex}
className={({ isActive }) =>
`menu-dropdown-item ${isActive ? 'active' : ''}`
}
onClick={onItemClick}
>
<span className="menu-dropdown-item-prefix" aria-hidden="true">
</span>
{item.label}
</NavLink>
</li>
))}
</ul>
</div>
</li>
);
}

export function MenuDropdown({
title,
titleTo,
items,
onItemClick,
}: MenuDropdownProps) {
const [isExpanded, setIsExpanded] = useState(false);
const [isHovered, setIsHovered] = useState(false);
const contentRef = useRef<HTMLDivElement>(null);
const [height, setHeight] = useState(0);
const location = useLocation();
const navigate = useNavigate();
const contentId = useId();

const isTitleActive = location.pathname === titleTo;
Comment thread
hyochan marked this conversation as resolved.
const isChildActive = items.some((item) => location.pathname === item.to);
const isChildActive = items.some((entry) =>
isGroup(entry)
? entry.items.some((i) => location.pathname === i.to)
: location.pathname === entry.to
);
const isGroupActive = isTitleActive || isChildActive;

useEffect(() => {
if (contentRef.current) {
setHeight(isExpanded ? contentRef.current.scrollHeight : 0);
}
}, [isExpanded]);

useEffect(() => {
// Auto-expand when navigating to the title page or any child
if (isGroupActive) {
setIsExpanded(true);
}
}, [isGroupActive]);

// Title click: always navigate + close the mobile drawer. Collapsing
// is handled exclusively by the dedicated chevron toggle so screen-
// reader semantics stay clean (the title is a nav control, not a
// disclosure control).
const handleTitleClick = () => {
// Always navigate and expand — never collapse from title click
setIsExpanded(true);
navigate(titleTo);
onItemClick?.();
};
Comment thread
hyochan marked this conversation as resolved.

const toggleExpanded = () => {
setIsExpanded(!isExpanded);
};
const toggleExpanded = () => setIsExpanded((v) => !v);

return (
<li className="menu-dropdown">
<div
className={`menu-dropdown-header ${isTitleActive ? 'active' : isChildActive ? 'group-active' : ''}`}
>
<button
type="button"
onClick={handleTitleClick}
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
className={`menu-dropdown-title ${isTitleActive ? 'active' : ''}`}
style={{
color:
isTitleActive || isHovered ? 'var(--primary-color)' : 'inherit',
}}
>
{title}
</button>
Comment thread
hyochan marked this conversation as resolved.
Comment thread
hyochan marked this conversation as resolved.
Comment thread
hyochan marked this conversation as resolved.
Comment thread
hyochan marked this conversation as resolved.
<button
type="button"
onClick={toggleExpanded}
className="menu-dropdown-toggle"
style={{
transform: isExpanded ? 'rotate(90deg)' : 'rotate(0deg)',
}}
aria-label={`Toggle ${title} submenu`}
aria-expanded={isExpanded}
aria-controls={contentId}
>
<Chevron isExpanded={isExpanded} />
</button>
</div>
<div
ref={contentRef}
id={contentId}
className="menu-dropdown-content"
style={{
maxHeight: `${height}px`,
}}
data-expanded={isExpanded}
Comment thread
hyochan marked this conversation as resolved.
aria-hidden={!isExpanded}
Comment thread
hyochan marked this conversation as resolved.
Comment thread
hyochan marked this conversation as resolved.
Comment thread
hyochan marked this conversation as resolved.
Comment thread
hyochan marked this conversation as resolved.
Comment thread
hyochan marked this conversation as resolved.
>
<ul className="menu-dropdown-items">
{items.map((item) => (
<li key={item.to}>
<NavLink
to={item.to}
className={({ isActive }) =>
`menu-dropdown-item ${isActive ? 'active' : ''}`
}
onClick={onItemClick}
>
<span className="menu-dropdown-item-prefix">└</span>
{item.label}
</NavLink>
</li>
))}
{items.map((entry) =>
isGroup(entry) ? (
<SubMenu
key={`${titleTo}::group::${entry.label.replace(/\s+/g, '-').toLowerCase()}`}
group={entry}
onItemClick={onItemClick}
parentExpanded={isExpanded}
/>
Comment thread
hyochan marked this conversation as resolved.
) : (
<li key={entry.to}>
<NavLink
to={entry.to}
tabIndex={isExpanded ? 0 : -1}
className={({ isActive }) =>
`menu-dropdown-item ${isActive ? 'active' : ''}`
}
onClick={onItemClick}
>
<span
className="menu-dropdown-item-prefix"
aria-hidden="true"
>
</span>
{entry.label}
</NavLink>
</li>
)
)}
</ul>
</div>
</li>
Expand Down
Loading
Loading