Skip to content

feat: Improve tabs on mobile#897

Open
rogaldh wants to merge 13 commits intosolana-foundation:masterfrom
hoodieshq:development-improve-tabs-on-mobile
Open

feat: Improve tabs on mobile#897
rogaldh wants to merge 13 commits intosolana-foundation:masterfrom
hoodieshq:development-improve-tabs-on-mobile

Conversation

@rogaldh
Copy link
Contributor

@rogaldh rogaldh commented Mar 21, 2026

Description

Improve tabs to make them work better on mobile by replacing plain tabs with <select>

Type of change

  • Bug fix
  • New feature

Screenshots

Desktop:

image

Mobile:

image image

Testing

All tests work.
stories are provided that perform checks for the proper number of tabs according to the tab nature (static/dynamic)

Related Issues

HOO-107
hoodieshq#50 hoodieshq#49 hoodieshq#48 hoodieshq#46 hoodieshq#57

Checklist

  • My code follows the project's style guidelines
  • I have added tests that prove my fix/feature works
  • All tests pass locally and in CI
  • I have run build:info script to update build information
  • CI/CD checks pass
  • I have included screenshots for protocol screens (if applicable)

@vercel
Copy link

vercel bot commented Mar 21, 2026

@rogaldh is attempting to deploy a commit to the Solana Foundation Team on Vercel.

A member of the Team first needs to authorize it.

@greptile-apps
Copy link
Contributor

greptile-apps bot commented Mar 21, 2026

Greptile Summary

This PR replaces the legacy Bootstrap tab list with a new navigation-tabs shared UI module that renders a <select> dropdown on mobile and the existing link-based tablist on desktop, improving usability on small screens.

Key changes:

  • New BaseNavigationTabs, NavigationTabs, NavigationTabLink, and TabLink components with a context-based dynamic tab registration system (useTabRegistration), enabling async/conditional tabs to register themselves without prop drilling.
  • allTabs deduplication in BaseNavigationTabs correctly filters registered tabs against staticPaths, preventing duplicate <option> entries in the mobile select.
  • address/[address]/layout.tsx is significantly simplified: the old Tab/TabComponent/slug types are removed; async tabs (CompressedNftTabs, ProgramMultisigTab, AccountDataTab) are expressed as NavigationTabLink children rendered inside Suspense.
  • block/[slot]/layout.tsx migrated cleanly with no behavioural changes.
  • Previous concern about router.push without scroll: false on mobile select has been addressed.
  • The stale TABS_LOOKUP entry for NFToken collections (which had an incorrect path pointing to a non-existent route) has been removed in favour of the correct path used inline.
  • Acknowledged limitation: compressed NFT tabs are eagerly shown in the static list for any account with no raw/parsed data regardless of whether it is ultimately confirmed as a compressed NFT. A TODO comment documents the planned follow-up refactor.

Confidence Score: 4/5

  • Safe to merge with one minor efficiency fix remaining; all previously raised blocking concerns have been resolved.
  • The core mobile/desktop tab rendering is correct. Deduplication logic works. The { scroll: false } regression is fixed. The desktop duplicate tab issue for compressed NFTs is resolved. The one remaining new finding is a non-blocking P2: NavigationTabLink unnecessarily calls registerTab for paths already in staticPaths, causing extra re-renders. The acknowledged hasTokenMetadata duplicate-in-static-tabs edge case for Token-2022 NFT mint accounts is intentionally deferred. Convergence across multiple prior review rounds warrants a 4.
  • app/shared/ui/navigation-tabs/ui/NavigationTabLink.tsx — minor guard missing in useEffect for static paths

Important Files Changed

Filename Overview
app/shared/ui/navigation-tabs/ui/NavigationTabs.tsx Entry-point client component — derives activeValue from useSelectedLayoutSegment, wires up router.push with { scroll: false } for the mobile select, and delegates rendering to BaseNavigationTabs. Clean and correct.
app/shared/ui/navigation-tabs/ui/BaseNavigationTabs.tsx Renders both the mobile <select> and the desktop role="tablist". Deduplication logic (allTabs filters registeredTabs against staticPaths) is correct and prevents duplicate options in the select. No 'use client' directive, intentionally acceptable since it is only used inside client components.
app/shared/ui/navigation-tabs/ui/NavigationTabLink.tsx Registers/unregisters dynamic tabs via context. Correctly suppresses desktop rendering when path is already in staticPaths. Minor: registerTab is called even for static-path entries, causing unnecessary extra state updates (see inline comment).
app/shared/ui/navigation-tabs/model/navigation-tabs-context.ts Context and useTabRegistration hook. registerTab correctly deduplicates within registeredTabs. Stable useCallback refs with empty deps ensure no spurious re-registrations.
app/block/[slot]/layout.tsx Straightforward migration from <ul> tab list to NavigationTabs. Static TABS array is clean with path/title only. buildHref correctly uses pickClusterParams.
app/shared/ui/navigation-tabs/ui/stories/navigation-tabs.stories.tsx Good coverage: static tabs, many tabs, async children, mobile select interaction, and overlapping static/async tab deduplication. Play functions validate both desktop tablist and mobile select lengths and values.
app/styles.css Adds .navigation-tabs-select styles with negative margins to bleed to card edges, and a :has(.navigation-tabs-select) media-query rule adjusting header padding on mobile. Dead e-w-auto that was previously flagged has been removed.
app/features/token-extensions/use-token-extension-navigation.ts Imports AddressTabPath from the refactored layout. The TOKEN_EXTENSIONS constant now uses the exported type union correctly. No logic changes.
app/address/[address]/layout.tsx Major refactor: removes old Tab slug system, introduces AddressTab type, and migrates async conditional tabs to NavigationTabLink children. Correctly uses 'nftoken-collection-nfts' path matching the actual route directory.

Sequence Diagram

sequenceDiagram
    participant Layout as AddressLayout / BlockLayout
    participant NT as NavigationTabs (client)
    participant BNT as BaseNavigationTabs
    participant Ctx as NavigationTabsContext
    participant NTL as NavigationTabLink (async child)
    participant Router as Next.js Router

    Layout->>NT: tabs={staticTabs} buildHref={fn}
    NT->>NT: activeValue = useSelectedLayoutSegment() ?? ''
    NT->>BNT: tabs, activeValue, onSelectChange, buildHref
    BNT->>Ctx: provide {activeValue, buildHref, registerTab, staticPaths, unregisterTab}

    Note over BNT: Renders <select> (mobile) + tablist div (desktop)

    NTL->>Ctx: useNavigationTabsContext()
    NTL->>Ctx: registerTab({path, title}) via useEffect
    Ctx-->>BNT: registeredTabs state updated
    BNT->>BNT: allTabs = [...staticTabs, ...registeredTabs.filter(not in staticPaths)]
    BNT-->>BNT: <select> re-renders with allTabs options

    Note over BNT: Desktop: staticTabs.map(TabLink) + {children} (NavigationTabLinks)
    Note over BNT: Mobile: allTabs.map(option) in <select>

    Router->>NT: user selects option (mobile)
    NT->>Router: router.push(buildHref(path), {scroll: false})
Loading

Reviews (5): Last reviewed commit: "resolve comments" | Re-trigger Greptile

@rogaldh
Copy link
Contributor Author

rogaldh commented Mar 21, 2026

@greptile-apps check again

@vercel
Copy link

vercel bot commented Mar 21, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
explorer Ready Ready Preview, Comment Mar 21, 2026 2:44am

Request Review

@rogaldh
Copy link
Contributor Author

rogaldh commented Mar 21, 2026

@greptile-apps check again

@rogaldh
Copy link
Contributor Author

rogaldh commented Mar 21, 2026

@greptile-apps check again

import React from 'react';

import { cn } from '@/app/components/shared/utils';
import { useNavigationTabsContext } from '@/app/entities/navigation-tabs/model/navigation-tabs-context';
Copy link
Contributor

Choose a reason for hiding this comment

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

suggestion: While there’s nothing critically wrong here, this does not align with classic FSD principles. According to FSD, entities should represent a domain concept (e.g. address, transaction, etc.) rather than merely grouping UI atoms. This should be categorized under shared, per the methodology. Additionally, we should never expose module internals (except for shared). If there’s concern about making the barrel too large, it may indicate that the entity abstraction should be reevaluated.

Copy link
Contributor

Choose a reason for hiding this comment

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

done

React.useEffect(() => {
ctx.registerTab({ path, title });
return () => ctx.unregisterTab(path);
// registerTab/unregisterTab are stable (useCallback with []), ctx excluded intentionally
Copy link
Contributor

Choose a reason for hiding this comment

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

question: Assuming these are stable, could we destructure registerTab and unregisterTab and create a proper dependency list?

Copy link
Contributor

Choose a reason for hiding this comment

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

done

activeValue: 'rewards',
tabs: BLOCK_TABS,
},
parameters: {
Copy link
Contributor

Choose a reason for hiding this comment

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

issue: Doesn't work, should be:

    globals: {
        viewport: { value: 'mobile1', isRotated: false },
    },

Copy link
Contributor

Choose a reason for hiding this comment

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

done

e-bg-heavy-metal-900 e-px-4 e-py-2.5 e-text-sm
e-text-neutral-200 e-outline-none;
margin-left: -12px;
margin-right: -12px;
Copy link
Contributor

Choose a reason for hiding this comment

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

suggestion: Perhaps these hacks should have a comment explaining what they depend on

Copy link
Contributor

Choose a reason for hiding this comment

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

done

const tabComponents = getTabs(pubkey, account).concat(getCustomLinkedTabs(pubkey, account));
const navigationTabs = getNavigationTabs(pubkey, account);

if (tab && tabComponents.filter(tabComponent => tabComponent.tab.slug === tab).length === 0) {
Copy link
Contributor

Choose a reason for hiding this comment

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

This guard is gone, please rollback those changes

rogaldh and others added 11 commits March 24, 2026 12:47
@C0mberry C0mberry force-pushed the development-improve-tabs-on-mobile branch from a8e8d13 to 9e3d12c Compare March 24, 2026 11:50
@C0mberry
Copy link
Contributor

@greptile-apps check again

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants