scrolling mobile tabs
This commit is contained in:
parent
95bc49a6bd
commit
f8205ea52b
|
@ -82,7 +82,7 @@ const MobileTradePage = () => {
|
|||
</div>
|
||||
<SwipeableTabs
|
||||
onChange={handleChangeViewIndex}
|
||||
tabs={TABS}
|
||||
items={TABS}
|
||||
tabIndex={viewIndex}
|
||||
/>
|
||||
<Swipeable index={viewIndex} onChangeIndex={handleChangeViewIndex}>
|
||||
|
|
|
@ -1,38 +1,217 @@
|
|||
import { ChevronLeftIcon, ChevronRightIcon } from '@heroicons/react/outline'
|
||||
import useDrag from 'hooks/useDrag'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
import React, { useContext, useEffect, useState } from 'react'
|
||||
import {
|
||||
ScrollMenu,
|
||||
VisibilityContext,
|
||||
getItemsPos,
|
||||
slidingWindow,
|
||||
} from 'react-horizontal-scrolling-menu'
|
||||
|
||||
const SwipeableTabs = ({ onChange, tabs, tabIndex }) => {
|
||||
const { t } = useTranslation('common')
|
||||
type scrollVisibilityApiType = React.ContextType<typeof VisibilityContext>
|
||||
|
||||
function SwipeableTabs({ items, onChange, tabIndex }) {
|
||||
const { dragStart, dragStop, dragMove, dragging } = useDrag()
|
||||
|
||||
const handleDrag =
|
||||
({ scrollContainer }: scrollVisibilityApiType) =>
|
||||
(ev: React.MouseEvent) =>
|
||||
dragMove(ev, (posDiff) => {
|
||||
if (scrollContainer.current) {
|
||||
scrollContainer.current.scrollLeft += posDiff
|
||||
}
|
||||
})
|
||||
|
||||
const handleItemClick =
|
||||
(itemId: string) =>
|
||||
({ getItemById, scrollToItem }: scrollVisibilityApiType) => {
|
||||
if (dragging) {
|
||||
return false
|
||||
}
|
||||
onChange(parseInt(itemId))
|
||||
scrollToItem(getItemById(itemId), 'smooth', 'center', 'nearest')
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`relative mb-4 border-b border-th-fgd-4`}>
|
||||
<div
|
||||
className={`default-transition absolute bottom-[-1px] left-0 h-0.5 bg-th-primary`}
|
||||
style={{
|
||||
maxWidth: '176px',
|
||||
transform: `translateX(${tabIndex * 100}%)`,
|
||||
width: `${100 / tabs.length}%`,
|
||||
}}
|
||||
/>
|
||||
<nav className="-mb-px flex" aria-label="Tabs">
|
||||
{tabs.map((tabName, i) => (
|
||||
<a
|
||||
key={tabName}
|
||||
onClick={() => onChange(i)}
|
||||
className={`default-transition relative flex cursor-pointer justify-center whitespace-nowrap pb-4 pt-2 font-semibold hover:opacity-100
|
||||
${
|
||||
tabIndex === i
|
||||
? `text-th-primary`
|
||||
: `text-th-fgd-4 hover:text-th-primary`
|
||||
}
|
||||
`}
|
||||
style={{ width: `${100 / tabs.length}%`, maxWidth: '176px' }}
|
||||
>
|
||||
{t(tabName.toLowerCase().replace(' ', '-'))}
|
||||
</a>
|
||||
<div
|
||||
onMouseLeave={dragStop}
|
||||
className="thin-scroll relative mb-4 border-b border-th-fgd-4 px-4"
|
||||
>
|
||||
<ScrollMenu
|
||||
LeftArrow={LeftArrow}
|
||||
RightArrow={RightArrow}
|
||||
onWheel={onWheel}
|
||||
onMouseDown={() => dragStart}
|
||||
onMouseUp={({
|
||||
getItemById,
|
||||
scrollToItem,
|
||||
visibleItems,
|
||||
}: scrollVisibilityApiType) =>
|
||||
() => {
|
||||
dragStop()
|
||||
const { center } = getItemsPos(visibleItems)
|
||||
scrollToItem(getItemById(center), 'smooth', 'center')
|
||||
}}
|
||||
options={{ throttle: 0 }}
|
||||
onMouseMove={handleDrag}
|
||||
onUpdate={({
|
||||
getItemById,
|
||||
scrollToItem,
|
||||
visibleItems,
|
||||
}: scrollVisibilityApiType) =>
|
||||
() => {
|
||||
dragStop()
|
||||
const { center } = getItemsPos(visibleItems)
|
||||
scrollToItem(getItemById(center), 'smooth', 'center')
|
||||
}}
|
||||
>
|
||||
{items.map((item, i) => (
|
||||
<Card
|
||||
title={item}
|
||||
itemId={i.toString()}
|
||||
key={item}
|
||||
onClick={handleItemClick(i.toString())}
|
||||
selected={i === tabIndex}
|
||||
/>
|
||||
))}
|
||||
</nav>
|
||||
</ScrollMenu>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default SwipeableTabs
|
||||
|
||||
function onWheel(
|
||||
{ getItemById, items, visibleItems, scrollToItem }: scrollVisibilityApiType,
|
||||
ev: React.WheelEvent
|
||||
): void {
|
||||
const isThouchpad = Math.abs(ev.deltaX) !== 0 || Math.abs(ev.deltaY) < 15
|
||||
|
||||
if (isThouchpad) {
|
||||
ev.stopPropagation()
|
||||
return
|
||||
}
|
||||
|
||||
if (ev.deltaY < 0) {
|
||||
const nextGroupItems = slidingWindow(
|
||||
items.toItemsKeys(),
|
||||
visibleItems
|
||||
).next()
|
||||
const { center } = getItemsPos(nextGroupItems)
|
||||
scrollToItem(getItemById(center), 'smooth', 'center')
|
||||
} else if (ev.deltaY > 0) {
|
||||
const prevGroupItems = slidingWindow(
|
||||
items.toItemsKeys(),
|
||||
visibleItems
|
||||
).prev()
|
||||
const { center } = getItemsPos(prevGroupItems)
|
||||
scrollToItem(getItemById(center), 'smooth', 'center')
|
||||
}
|
||||
}
|
||||
|
||||
function Card({
|
||||
selected,
|
||||
onClick,
|
||||
title,
|
||||
}: {
|
||||
itemId: string
|
||||
selected: boolean
|
||||
onClick: (x) => void
|
||||
title: string
|
||||
}) {
|
||||
const { t } = useTranslation('common')
|
||||
const visibility = React.useContext(VisibilityContext)
|
||||
|
||||
useEffect(() => {
|
||||
if (selected) {
|
||||
onClick(visibility)
|
||||
}
|
||||
}, [selected])
|
||||
|
||||
return (
|
||||
<div
|
||||
onClick={() => onClick(visibility)}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
className={`flex h-10 w-32 items-center justify-center ${
|
||||
selected
|
||||
? 'border-b-2 border-th-primary text-th-primary'
|
||||
: 'border-b-2 border-transparent text-th-fgd-3'
|
||||
}`}
|
||||
>
|
||||
{t(title)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default SwipeableTabs
|
||||
function Arrow({
|
||||
children,
|
||||
disabled,
|
||||
onClick,
|
||||
className,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
disabled: boolean
|
||||
onClick: VoidFunction
|
||||
className?: string
|
||||
}) {
|
||||
return (
|
||||
<button
|
||||
disabled={disabled}
|
||||
onClick={onClick}
|
||||
className={`top-1/2 -translate-y-1/2 transform rounded-none ${
|
||||
disabled ? 'hidden' : 'absolute'
|
||||
} ${className}`}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
function LeftArrow() {
|
||||
const {
|
||||
isFirstItemVisible,
|
||||
scrollPrev,
|
||||
visibleItemsWithoutSeparators,
|
||||
initComplete,
|
||||
} = useContext(VisibilityContext)
|
||||
|
||||
const [disabled, setDisabled] = useState(
|
||||
!initComplete || (initComplete && isFirstItemVisible)
|
||||
)
|
||||
useEffect(() => {
|
||||
if (visibleItemsWithoutSeparators.length) {
|
||||
setDisabled(isFirstItemVisible)
|
||||
}
|
||||
}, [isFirstItemVisible, visibleItemsWithoutSeparators])
|
||||
|
||||
return (
|
||||
<Arrow disabled={disabled} onClick={() => scrollPrev()} className="-left-2">
|
||||
<ChevronLeftIcon className="h-5 w-5 text-th-fgd-3" />
|
||||
</Arrow>
|
||||
)
|
||||
}
|
||||
|
||||
function RightArrow() {
|
||||
const { isLastItemVisible, scrollNext, visibleItemsWithoutSeparators } =
|
||||
useContext(VisibilityContext)
|
||||
|
||||
const [disabled, setDisabled] = useState(
|
||||
!visibleItemsWithoutSeparators.length && isLastItemVisible
|
||||
)
|
||||
useEffect(() => {
|
||||
if (visibleItemsWithoutSeparators.length) {
|
||||
setDisabled(isLastItemVisible)
|
||||
}
|
||||
}, [isLastItemVisible, visibleItemsWithoutSeparators])
|
||||
|
||||
return (
|
||||
<Arrow
|
||||
disabled={disabled}
|
||||
onClick={() => scrollNext()}
|
||||
className="-right-2"
|
||||
>
|
||||
<ChevronRightIcon className="h-5 w-5 text-th-fgd-3" />
|
||||
</Arrow>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,46 @@
|
|||
import React from 'react'
|
||||
|
||||
export default function useDrag() {
|
||||
const [clicked, setClicked] = React.useState(false)
|
||||
const [dragging, setDragging] = React.useState(false)
|
||||
const position = React.useRef(0)
|
||||
|
||||
const dragStart = React.useCallback((ev: React.MouseEvent) => {
|
||||
position.current = ev.clientX
|
||||
setClicked(true)
|
||||
}, [])
|
||||
|
||||
const dragStop = React.useCallback(
|
||||
() =>
|
||||
// NOTE: need some delay so item under cursor won't be clicked
|
||||
window.requestAnimationFrame(() => {
|
||||
setDragging(false)
|
||||
setClicked(false)
|
||||
}),
|
||||
[]
|
||||
)
|
||||
|
||||
const dragMove = (ev: React.MouseEvent, cb: (posDif: number) => void) => {
|
||||
const newDiff = position.current - ev.clientX
|
||||
|
||||
const movedEnough = Math.abs(newDiff) > 5
|
||||
|
||||
if (clicked && movedEnough) {
|
||||
setDragging(true)
|
||||
}
|
||||
|
||||
if (dragging && movedEnough) {
|
||||
position.current = ev.clientX
|
||||
cb(newDiff)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
dragStart,
|
||||
dragStop,
|
||||
dragMove,
|
||||
dragging,
|
||||
position,
|
||||
setDragging,
|
||||
}
|
||||
}
|
|
@ -49,6 +49,7 @@
|
|||
"react-cool-dimensions": "^2.0.7",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-grid-layout": "^1.3.3",
|
||||
"react-horizontal-scrolling-menu": "^2.8.2",
|
||||
"react-nice-dates": "^3.1.0",
|
||||
"react-portal": "^4.2.1",
|
||||
"react-qr-code": "^2.0.3",
|
||||
|
|
|
@ -57,6 +57,7 @@ import { handleWalletConnect } from 'components/ConnectWalletButton'
|
|||
import { MangoAccountLookup } from 'components/account_page/MangoAccountLookup'
|
||||
import NftProfilePicModal from 'components/NftProfilePicModal'
|
||||
import ProfileImage from 'components/ProfileImage'
|
||||
import SwipeableTabs from 'components/mobile/SwipeableTabs'
|
||||
|
||||
export async function getStaticProps({ locale }) {
|
||||
return {
|
||||
|
@ -448,40 +449,47 @@ export default function Account() {
|
|||
) : null}
|
||||
</div>
|
||||
<div className="md:rounded-lg md:bg-th-bkg-2 md:p-6">
|
||||
{mangoAccount ? (
|
||||
<Tabs
|
||||
activeTab={activeTab}
|
||||
onChange={handleTabChange}
|
||||
tabs={TABS}
|
||||
/>
|
||||
) : null}
|
||||
{mangoAccount ? (
|
||||
!isMobile ? (
|
||||
<TabContent activeTab={activeTab} />
|
||||
<>
|
||||
<Tabs
|
||||
activeTab={activeTab}
|
||||
onChange={handleTabChange}
|
||||
tabs={TABS}
|
||||
/>
|
||||
<TabContent activeTab={activeTab} />
|
||||
</>
|
||||
) : (
|
||||
<Swipeable
|
||||
index={viewIndex}
|
||||
onChangeIndex={handleChangeViewIndex}
|
||||
>
|
||||
<div>
|
||||
<AccountOverview />
|
||||
</div>
|
||||
<div>
|
||||
<AccountOrders />
|
||||
</div>
|
||||
<div>
|
||||
<AccountHistory />
|
||||
</div>
|
||||
<div>
|
||||
<AccountInterest />
|
||||
</div>
|
||||
<div>
|
||||
<AccountFunding />
|
||||
</div>
|
||||
<div>
|
||||
<AccountPerformancePerToken />
|
||||
</div>
|
||||
</Swipeable>
|
||||
<>
|
||||
<SwipeableTabs
|
||||
onChange={handleChangeViewIndex}
|
||||
items={TABS}
|
||||
tabIndex={viewIndex}
|
||||
/>
|
||||
<Swipeable
|
||||
index={viewIndex}
|
||||
onChangeIndex={handleChangeViewIndex}
|
||||
>
|
||||
<div>
|
||||
<AccountOverview />
|
||||
</div>
|
||||
<div>
|
||||
<AccountOrders />
|
||||
</div>
|
||||
<div>
|
||||
<AccountHistory />
|
||||
</div>
|
||||
<div>
|
||||
<AccountInterest />
|
||||
</div>
|
||||
<div>
|
||||
<AccountFunding />
|
||||
</div>
|
||||
<div>
|
||||
<AccountPerformancePerToken />
|
||||
</div>
|
||||
</Swipeable>
|
||||
</>
|
||||
)
|
||||
) : connected ? (
|
||||
isLoading ? (
|
||||
|
|
|
@ -54,7 +54,7 @@ export default function Markets() {
|
|||
) : (
|
||||
<SwipeableTabs
|
||||
onChange={handleChangeViewIndex}
|
||||
tabs={TABS}
|
||||
items={TABS}
|
||||
tabIndex={viewIndex}
|
||||
/>
|
||||
)}
|
||||
|
|
|
@ -62,7 +62,7 @@ export default function StatsPage() {
|
|||
) : (
|
||||
<SwipeableTabs
|
||||
onChange={handleChangeViewIndex}
|
||||
tabs={TABS}
|
||||
items={TABS}
|
||||
tabIndex={viewIndex}
|
||||
/>
|
||||
)}
|
||||
|
|
26
yarn.lock
26
yarn.lock
|
@ -3229,6 +3229,11 @@ commander@^8.0.0, commander@^8.3.0:
|
|||
resolved "https://registry.yarnpkg.com/commander/-/commander-8.3.0.tgz#4837ea1b2da67b9c616a67afbb0fafee567bca66"
|
||||
integrity sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==
|
||||
|
||||
compute-scroll-into-view@^1.0.17:
|
||||
version "1.0.17"
|
||||
resolved "https://registry.yarnpkg.com/compute-scroll-into-view/-/compute-scroll-into-view-1.0.17.tgz#6a88f18acd9d42e9cf4baa6bec7e0522607ab7ab"
|
||||
integrity sha512-j4dx+Fb0URmzbwwMUrhqWM2BEWHdFGx+qZ9qqASHRPqvTYdqvWnHg0H1hIbcyLnvgnoNAVMlwkepyqM3DaIFUg==
|
||||
|
||||
concat-map@0.0.1:
|
||||
version "0.0.1"
|
||||
resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
|
||||
|
@ -5602,6 +5607,13 @@ react-grid-layout@^1.3.3:
|
|||
react-draggable "^4.0.0"
|
||||
react-resizable "^3.0.4"
|
||||
|
||||
react-horizontal-scrolling-menu@^2.8.2:
|
||||
version "2.8.2"
|
||||
resolved "https://registry.yarnpkg.com/react-horizontal-scrolling-menu/-/react-horizontal-scrolling-menu-2.8.2.tgz#cb7bc7c6798e0cd9203555cf4e6fcffb5f0ba0f2"
|
||||
integrity sha512-OmCQDONx/4TnyxQr+RoxP7AwSPQxdQGkFnWuCknF3s2pySNAogL4AB1LQv6bUrXIwFMXdrJBqxFeq3tZ24J7/w==
|
||||
dependencies:
|
||||
smooth-scroll-into-view-if-needed "^1.1.32"
|
||||
|
||||
react-i18next@^11.15.5:
|
||||
version "11.16.2"
|
||||
resolved "https://registry.yarnpkg.com/react-i18next/-/react-i18next-11.16.2.tgz#650b18c12a624057ee2651ba4b4a989b526be554"
|
||||
|
@ -6018,6 +6030,13 @@ scheduler@^0.20.2:
|
|||
loose-envify "^1.1.0"
|
||||
object-assign "^4.1.1"
|
||||
|
||||
scroll-into-view-if-needed@^2.2.28:
|
||||
version "2.2.29"
|
||||
resolved "https://registry.yarnpkg.com/scroll-into-view-if-needed/-/scroll-into-view-if-needed-2.2.29.tgz#551791a84b7e2287706511f8c68161e4990ab885"
|
||||
integrity sha512-hxpAR6AN+Gh53AdAimHM6C8oTN1ppwVZITihix+WqalywBeFcQ6LdQP5ABNl26nX8GTEL7VT+b8lKpdqq65wXg==
|
||||
dependencies:
|
||||
compute-scroll-into-view "^1.0.17"
|
||||
|
||||
scrypt-js@^3.0.0:
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/scrypt-js/-/scrypt-js-3.0.1.tgz#d314a57c2aef69d1ad98a138a21fe9eafa9ee312"
|
||||
|
@ -6145,6 +6164,13 @@ slice-ansi@^5.0.0:
|
|||
ansi-styles "^6.0.0"
|
||||
is-fullwidth-code-point "^4.0.0"
|
||||
|
||||
smooth-scroll-into-view-if-needed@^1.1.32:
|
||||
version "1.1.33"
|
||||
resolved "https://registry.yarnpkg.com/smooth-scroll-into-view-if-needed/-/smooth-scroll-into-view-if-needed-1.1.33.tgz#2c7b88c82784c69030cb0489b9df584e94e01533"
|
||||
integrity sha512-crS8NfAaoPrtVYOCMSAnO2vHRgUp22NiiDgEQ7YiaAy5xe2jmR19Jm+QdL8+97gO8ENd7PUyQIAQojJyIiyRHw==
|
||||
dependencies:
|
||||
scroll-into-view-if-needed "^2.2.28"
|
||||
|
||||
snake-case@^3.0.4:
|
||||
version "3.0.4"
|
||||
resolved "https://registry.yarnpkg.com/snake-case/-/snake-case-3.0.4.tgz#4f2bbd568e9935abdfd593f34c691dadb49c452c"
|
||||
|
|
Loading…
Reference in New Issue