-
Notifications
You must be signed in to change notification settings - Fork 99
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
asmyshlyaev177
committed
Nov 20, 2024
1 parent
fdde0ca
commit a8d487e
Showing
2 changed files
with
255 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,219 @@ | ||
import React from 'react'; | ||
import styled from 'styled-jss'; | ||
|
||
import { | ||
ScrollMenu, | ||
VisibilityContext, | ||
type publicApiType, | ||
} from 'react-horizontal-scrolling-menu'; | ||
|
||
import 'react-horizontal-scrolling-menu/dist/styles.css'; | ||
|
||
export function ScrollToItem() { | ||
const [items] = React.useState(() => getItems()); | ||
const [selected, setSelected] = React.useState<string[]>([]); | ||
|
||
const isItemSelected = (id: string): boolean => | ||
!!selected.find((el) => el === id); | ||
|
||
const handleItemClick = (itemId: string) => { | ||
const itemSelected = isItemSelected(itemId); | ||
|
||
setSelected((currentSelected: string[]) => | ||
itemSelected | ||
? currentSelected.filter((el) => el !== itemId) | ||
: currentSelected.concat(itemId), | ||
); | ||
}; | ||
|
||
const apiRef = React.useRef<publicApiType | null>(null); | ||
|
||
// TODO: fix bug with items | ||
React.useEffect(() => { | ||
if (!apiRef.current) return () => {}; | ||
|
||
const id = setTimeout(() => { | ||
const itemsList = [...apiRef.current.items.toItems()]; | ||
const itemKey = itemsList.find((el) => el.includes(5)); | ||
const item = apiRef.current.getItemById(itemKey); | ||
// const item = apiRef.current.getItemByIndex(5) // or by index | ||
apiRef.current.scrollToItem(item, 'auto', 'start'); | ||
}, 100); | ||
|
||
return () => clearTimeout(id); | ||
}, [apiRef.current]); | ||
|
||
return ( | ||
<ScrollMenu | ||
LeftArrow={LeftArrow} | ||
RightArrow={RightArrow} | ||
onWheel={onWheel} | ||
apiRef={apiRef} | ||
> | ||
{items.map(({ id }) => ( | ||
<Card | ||
title={id} | ||
itemId={id} // NOTE: itemId is required for track items | ||
key={id} | ||
onClick={() => handleItemClick(id)} | ||
selected={isItemSelected(id)} | ||
/> | ||
))} | ||
</ScrollMenu> | ||
); | ||
} | ||
|
||
export default ScrollToItem; | ||
|
||
function LeftArrow() { | ||
const visibility = React.useContext<publicApiType>(VisibilityContext); | ||
|
||
const disabled = visibility.useLeftArrowVisible(); | ||
|
||
return ( | ||
<Arrow | ||
disabled={disabled} | ||
onClick={() => visibility.scrollPrev()} | ||
testId="left-arrow" | ||
> | ||
Left | ||
</Arrow> | ||
); | ||
} | ||
|
||
function RightArrow() { | ||
const visibility = React.useContext<publicApiType>(VisibilityContext); | ||
|
||
const disabled = visibility.useRightArrowVisible(); | ||
|
||
return ( | ||
<Arrow | ||
disabled={disabled} | ||
onClick={() => visibility.scrollNext()} | ||
testId="right-arrow" | ||
> | ||
Right | ||
</Arrow> | ||
); | ||
} | ||
|
||
function Arrow({ | ||
children, | ||
disabled, | ||
onClick, | ||
className, | ||
testId, | ||
}: { | ||
children: React.ReactNode; | ||
disabled: boolean; | ||
onClick: VoidFunction; | ||
className?: string; | ||
testId: string; | ||
}) { | ||
return ( | ||
<ArrowButton | ||
disabled={disabled} | ||
onClick={onClick} | ||
className={'arrow' + `-${className}`} | ||
data-testid={testId} | ||
> | ||
{children} | ||
</ArrowButton> | ||
); | ||
} | ||
const ArrowButton = styled('button')({ | ||
cursor: 'pointer', | ||
display: 'flex', | ||
flexDirection: 'column', | ||
justifyContent: 'center', | ||
marginBottom: '2px', | ||
opacity: (props) => (props.disabled ? '0' : '1'), | ||
userSelect: 'none', | ||
borderRadius: '6px', | ||
borderWidth: '1px', | ||
}); | ||
|
||
function Card({ | ||
onClick, | ||
selected, | ||
title, | ||
itemId, | ||
}: { | ||
onClick: (context: publicApiType) => void; | ||
selected: boolean; | ||
title: string; | ||
itemId: string; | ||
}) { | ||
const visibility = React.useContext<publicApiType>(VisibilityContext); | ||
const isVisible = visibility.useIsVisible(itemId, true); | ||
|
||
return ( | ||
<CardBody | ||
data-cy={itemId} | ||
onClick={() => onClick(visibility)} | ||
onKeyDown={(ev: React.KeyboardEvent) => { | ||
ev.code === 'Enter' && onClick(visibility); | ||
}} | ||
data-testid="card" | ||
role="button" | ||
tabIndex={0} | ||
className="card" | ||
visible={isVisible} | ||
selected={selected} | ||
> | ||
<div className="header"> | ||
<div>{title}</div> | ||
<div className="visible">visible: {JSON.stringify(isVisible)}</div> | ||
<div className="selected">selected: {JSON.stringify(!!selected)}</div> | ||
</div> | ||
<div className="background" /> | ||
</CardBody> | ||
); | ||
} | ||
const CardBody = styled('div')({ | ||
border: '1px solid', | ||
display: 'inline-block', | ||
margin: '0 10px', | ||
width: '160px', | ||
userSelect: 'none', | ||
borderRadius: '8px', | ||
overflow: 'hidden', | ||
|
||
'& .header': { | ||
backgroundColor: 'white', | ||
}, | ||
|
||
'& .visible': { | ||
backgroundColor: (props) => (props.visible ? 'transparent' : 'gray'), | ||
}, | ||
|
||
'& .background': { | ||
backgroundColor: (props) => (props.selected ? 'green' : 'bisque'), | ||
height: '200px', | ||
}, | ||
}); | ||
|
||
const getId = (index: number) => `${'test'}${index}`; | ||
|
||
const getItems = () => | ||
Array(10) | ||
.fill(0) | ||
.map((_, ind) => ({ id: getId(ind) })); | ||
|
||
function onWheel(apiObj: publicApiType, ev: React.WheelEvent): void { | ||
// NOTE: no good standart way to distinguish touchpad scrolling gestures | ||
// but can assume that gesture will affect X axis, mouse scroll only Y axis | ||
// of if deltaY too small probably is it touchpad | ||
const isThouchpad = Math.abs(ev.deltaX) !== 0 || Math.abs(ev.deltaY) < 15; | ||
|
||
if (isThouchpad) { | ||
ev.stopPropagation(); | ||
return; | ||
} | ||
|
||
if (ev.deltaY < 0) { | ||
apiObj.scrollNext(); | ||
} else { | ||
apiObj.scrollPrev(); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
/* eslint-disable @typescript-eslint/ban-ts-comment */ | ||
import { type Meta } from '@storybook/react'; | ||
import React from 'react'; | ||
import { createLiveEditStory } from 'storybook-addon-code-editor'; | ||
|
||
import { ScrollMenu } from '../../src/index'; | ||
import { SizeWrapper } from '../SizeWrapper'; | ||
import { availableImports } from '../availableImports'; | ||
import { setupEditor } from '../setupEditor'; | ||
import { ScrollTest } from '../test'; | ||
|
||
import Example from './ScrollToItem.source'; | ||
// @ts-ignore | ||
import ExampleRaw from './ScrollToItem.source.tsx?raw'; | ||
|
||
const meta: Meta<typeof ScrollMenu> = { | ||
title: 'Examples/ScrollToItem', | ||
component: Example, | ||
decorators: [ | ||
(Story) => ( | ||
<SizeWrapper> | ||
<Story /> | ||
</SizeWrapper> | ||
), | ||
], | ||
}; | ||
|
||
export default meta; | ||
|
||
export const ScrollToItem = createLiveEditStory({ | ||
code: ExampleRaw, | ||
availableImports, | ||
modifyEditor: setupEditor, | ||
}); | ||
|
||
export const Test = ScrollTest(); |