Skip to content

Commit 9abced5

Browse files
Fix mobile UX issues (#2445)
* Fix mobile UX issues * change import * update comments
1 parent 5ca5229 commit 9abced5

File tree

5 files changed

+261
-0
lines changed

5 files changed

+261
-0
lines changed

src/config/mobileProductsMenu.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
/**
2+
* Mobile Products Menu Configuration
3+
*
4+
* This configuration defines a simple flat list of product items
5+
* displayed in the mobile sidebar under "Products".
6+
*/
7+
8+
export interface MobileProductItem {
9+
label: string
10+
href: string
11+
}
12+
13+
export const mobileProductsMenu: MobileProductItem[] = [
14+
{
15+
label: 'MetaMask SDK',
16+
href: '/sdk/',
17+
},
18+
{
19+
label: 'Wallet API',
20+
href: '/wallet/',
21+
},
22+
{
23+
label: 'Embedded Wallets',
24+
href: '/embedded-wallets',
25+
},
26+
{
27+
label: 'Delegation Toolkit',
28+
href: '/delegation-toolkit',
29+
},
30+
{
31+
label: 'Snaps',
32+
href: '/snaps',
33+
},
34+
{
35+
label: 'Services',
36+
href: '/services',
37+
},
38+
{
39+
label: 'Developer dashboard',
40+
href: '/developer-tools/dashboard',
41+
},
42+
]
43+

src/theme/MDXPage/styles.module.css

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,12 @@
134134
gap: 8px;
135135
}
136136

137+
@media only screen and (width < 768px) {
138+
.bookmarkButton {
139+
display: none;
140+
}
141+
}
142+
137143
.bottomMenu {
138144
display: flex;
139145
flex-direction: row;
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
/**
2+
* Custom PrimaryMenu for Mobile Sidebar
3+
*
4+
* This component handles the Products dropdown specially by rendering
5+
* a custom mobile-friendly menu structure instead of the HTML dropdown.
6+
*/
7+
8+
import React, {type ReactNode} from 'react'
9+
import {useThemeConfig} from '@docusaurus/theme-common'
10+
import {useNavbarMobileSidebar} from '@docusaurus/theme-common/internal'
11+
import NavbarItem, {type Props as NavbarItemConfig} from '@theme/NavbarItem'
12+
import ProductsMenuMobile from '../ProductsMenu'
13+
14+
function useNavbarItems() {
15+
// TODO temporary casting until ThemeConfig type is improved
16+
return useThemeConfig().navbar.items as NavbarItemConfig[]
17+
}
18+
19+
// The primary menu displays the navbar items
20+
export default function NavbarMobilePrimaryMenu(): ReactNode {
21+
const mobileSidebar = useNavbarMobileSidebar()
22+
const items = useNavbarItems()
23+
24+
return (
25+
<ul className="menu__list">
26+
{items.map((item, i) => {
27+
// Check if this is the Products dropdown
28+
const isProductsDropdown =
29+
item.type === 'dropdown' &&
30+
item.label === 'Products' &&
31+
item.items?.some((subItem) => subItem.type === 'html')
32+
33+
// If it's the Products dropdown, render custom component
34+
if (isProductsDropdown) {
35+
return (
36+
<ProductsMenuMobile
37+
key={i}
38+
onItemClick={() => mobileSidebar.toggle()}
39+
/>
40+
)
41+
}
42+
43+
// Otherwise, render normally
44+
return (
45+
<NavbarItem
46+
mobile
47+
{...item}
48+
onClick={() => mobileSidebar.toggle()}
49+
key={i}
50+
/>
51+
)
52+
})}
53+
</ul>
54+
)
55+
}
56+
Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
/**
2+
* Custom Products Menu Component for Mobile
3+
*
4+
* This component renders a simple flat list of product items
5+
* in the mobile sidebar under an expandable "Products" menu.
6+
*/
7+
8+
import React, {useEffect} from 'react'
9+
import clsx from 'clsx'
10+
import {
11+
useCollapsible,
12+
Collapsible,
13+
} from '@docusaurus/theme-common'
14+
import {useLocalPathname} from '@docusaurus/theme-common/internal'
15+
import {translate} from '@docusaurus/Translate'
16+
import Link from '@docusaurus/Link'
17+
import {mobileProductsMenu} from '@site/src/config/mobileProductsMenu'
18+
import styles from './styles.module.css'
19+
20+
function CollapseButton({
21+
collapsed,
22+
onClick,
23+
}: {
24+
collapsed: boolean
25+
onClick: React.MouseEventHandler<HTMLButtonElement>
26+
}) {
27+
return (
28+
<button
29+
aria-label={
30+
collapsed
31+
? translate({
32+
id: 'theme.navbar.mobileDropdown.collapseButton.expandAriaLabel',
33+
message: 'Expand the dropdown',
34+
description:
35+
'The ARIA label of the button to expand the mobile dropdown navbar item',
36+
})
37+
: translate({
38+
id: 'theme.navbar.mobileDropdown.collapseButton.collapseAriaLabel',
39+
message: 'Collapse the dropdown',
40+
description:
41+
'The ARIA label of the button to collapse the mobile dropdown navbar item',
42+
})
43+
}
44+
aria-expanded={!collapsed}
45+
type="button"
46+
className="clean-btn menu__caret"
47+
onClick={onClick}
48+
/>
49+
)
50+
}
51+
52+
function useItemCollapsible({active}: {active: boolean}) {
53+
const {collapsed, toggleCollapsed, setCollapsed} = useCollapsible({
54+
initialState: () => !active,
55+
})
56+
57+
// Expand if any item active after a navigation
58+
useEffect(() => {
59+
if (active) {
60+
setCollapsed(false)
61+
}
62+
}, [active, setCollapsed])
63+
64+
return {
65+
collapsed,
66+
toggleCollapsed,
67+
}
68+
}
69+
70+
function isPathActive(pathname: string, href: string): boolean {
71+
// Normalize paths by removing trailing slashes
72+
const normalizedPathname = pathname.replace(/\/$/, '') || '/'
73+
const normalizedHref = href.replace(/\/$/, '') || '/'
74+
75+
// Check exact match or if pathname starts with href
76+
return normalizedPathname === normalizedHref || normalizedPathname.startsWith(normalizedHref + '/')
77+
}
78+
79+
export default function ProductsMenuMobile({
80+
onItemClick,
81+
}: {
82+
onItemClick: () => void
83+
}): React.ReactElement {
84+
const localPathname = useLocalPathname()
85+
86+
// Check if any product item is active
87+
const hasActiveItem = mobileProductsMenu.some((item) =>
88+
isPathActive(localPathname, item.href)
89+
)
90+
91+
const {collapsed, toggleCollapsed} = useItemCollapsible({
92+
active: hasActiveItem,
93+
})
94+
95+
return (
96+
<li
97+
className={clsx('menu__list-item', {
98+
'menu__list-item--collapsed': collapsed,
99+
})}>
100+
<div
101+
className={clsx('menu__list-item-collapsible', {
102+
'menu__list-item-collapsible--active': hasActiveItem,
103+
})}>
104+
<span
105+
className={clsx(
106+
styles.dropdownNavbarItemMobile,
107+
'menu__link menu__link--sublist'
108+
)}
109+
role="button"
110+
tabIndex={0}
111+
onClick={(e) => {
112+
e.preventDefault()
113+
toggleCollapsed()
114+
}}
115+
onKeyDown={(e) => {
116+
if (e.key === 'Enter' || e.key === ' ') {
117+
e.preventDefault()
118+
toggleCollapsed()
119+
}
120+
}}>
121+
Products
122+
</span>
123+
<CollapseButton
124+
collapsed={collapsed}
125+
onClick={(e) => {
126+
e.preventDefault()
127+
toggleCollapsed()
128+
}}
129+
/>
130+
</div>
131+
132+
<Collapsible lazy as="ul" className="menu__list" collapsed={collapsed}>
133+
{mobileProductsMenu.map((item, i) => {
134+
const isItemActive = isPathActive(localPathname, item.href)
135+
return (
136+
<li key={i} className="menu__list-item">
137+
<Link
138+
to={item.href}
139+
className={clsx('menu__link', {
140+
'menu__link--active': isItemActive,
141+
})}
142+
onClick={onItemClick}>
143+
{item.label}
144+
</Link>
145+
</li>
146+
)
147+
})}
148+
</Collapsible>
149+
</li>
150+
)
151+
}
152+
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
.dropdownNavbarItemMobile {
2+
flex: 1;
3+
}
4+

0 commit comments

Comments
 (0)