refactor: use menu provider logic for player menus

This commit is contained in:
Tim 2024-02-20 02:29:55 +01:00
parent c3361481b4
commit 63da56b4a0
24 changed files with 274 additions and 114 deletions

10
package-lock.json generated
View file

@ -48,6 +48,7 @@
"@babel/preset-react": "7.16.0",
"@types/classnames": "^2.3.1",
"@types/react": "^18.2.9",
"@types/react-dom": "^18.2.19",
"babel-loader": "8.2.3",
"clean-webpack-plugin": "4.0.0",
"copy-webpack-plugin": "9.0.1",
@ -3265,6 +3266,15 @@
"csstype": "^3.0.2"
}
},
"node_modules/@types/react-dom": {
"version": "18.2.19",
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.2.19.tgz",
"integrity": "sha512-aZvQL6uUbIJpjZk4U8JZGbau9KDeAwMfmhyWorxgBkqDIEf6ROjRozcmPIicqsUwPUjbkDfHKgGee1Lq65APcA==",
"dev": true,
"dependencies": {
"@types/react": "*"
}
},
"node_modules/@types/resolve": {
"version": "1.17.1",
"dev": true,

View file

@ -51,6 +51,7 @@
"@babel/preset-react": "7.16.0",
"@types/classnames": "^2.3.1",
"@types/react": "^18.2.9",
"@types/react-dom": "^18.2.19",
"babel-loader": "8.2.3",
"clean-webpack-plugin": "4.0.0",
"copy-webpack-plugin": "9.0.1",

View file

@ -14,6 +14,7 @@ const ErrorDialog = require('./ErrorDialog');
const withProtectedRoutes = require('./withProtectedRoutes');
const routerViewsConfig = require('./routerViewsConfig');
const styles = require('./styles');
const { MenuProvider } = require('stremio/common');
const RouterWithProtectedRoutes = withCoreSuspender(withProtectedRoutes(Router));
@ -163,14 +164,16 @@ const App = () => {
:
<ToastProvider className={styles['toasts-container']}>
<TooltipProvider className={styles['tooltip-container']}>
<ServicesToaster />
<DeepLinkHandler />
<SearchParamsHandler />
<RouterWithProtectedRoutes
className={styles['router']}
viewsConfig={routerViewsConfig}
onPathNotMatch={onPathNotMatch}
/>
<MenuProvider className={styles['menu-container']}>
<ServicesToaster />
<DeepLinkHandler />
<SearchParamsHandler />
<RouterWithProtectedRoutes
className={styles['router']}
viewsConfig={routerViewsConfig}
onPathNotMatch={onPathNotMatch}
/>
</MenuProvider>
</TooltipProvider>
</ToastProvider>
:

View file

@ -103,12 +103,12 @@ html {
height: 100%;
.toasts-container {
z-index: 2;
position: absolute;
top: calc(1.2 * var(--horizontal-nav-bar-size));
right: 0;
bottom: calc(1.2 * var(--horizontal-nav-bar-size));
left: auto;
z-index: 1;
padding: 0 calc(0.5 * var(--horizontal-nav-bar-size));
overflow-y: auto;
scrollbar-width: none;
@ -120,6 +120,7 @@ html {
}
.tooltip-container {
z-index: 1;
height: 2.5rem;
display: flex;
align-items: center;
@ -137,6 +138,15 @@ html {
}
}
.menu-container {
z-index: 1;
position: absolute;
border-radius: var(--border-radius);
background-color: var(--modal-background-color);
box-shadow: 0 1.35rem 2.7rem rgba(0, 0, 0, 0.4), 0 1.1rem 0.85rem rgba(0, 0, 0, 0.2);
backdrop-filter: blur(15px);
}
.router {
width: 100%;
height: 100%;

52
src/common/Menu/Menu.tsx Normal file
View file

@ -0,0 +1,52 @@
// Copyright (C) 2017-2024 Smart code 203358507
import React, { useEffect, useRef } from 'react';
import useMenu from './useMenu';
import useKeyboardEvent from '../useKeyboardEvent';
import styles from './styles.less';
type Props = {
children: JSX.Element,
className: string,
shortcut: string,
align: 'left' | 'right',
};
const createId = () => (Math.random() + 1).toString(36).substring(7);
const Menu = ({ children, className, shortcut, align }: Props) => {
const menu = useMenu();
const id = useRef(createId());
const element = useRef<HTMLDivElement>(null);
const onToggle = () => {
menu.toggle(id.current);
};
useEffect(() => {
const parent = element.current?.parentElement;
parent.addEventListener('click', onToggle);
menu.create({
id: id.current,
className,
parent,
align,
});
return () => {
parent.removeEventListener('click', onToggle);
menu.remove(id.current);
};
}, []);
useKeyboardEvent(shortcut, onToggle, !shortcut);
return <>
<div ref={element} className={styles['menu-placeholder']} />
{menu.active?.id === id.current && menu.render(children)}
</>;
};
export default Menu;

View file

@ -0,0 +1,17 @@
// Copyright (C) 2017-2024 Smart code 203358507
import { createContext } from 'react';
import { type Menu } from './types';
type MenuContext = {
active?: Menu,
create: (menu: Omit<Menu, 'open'>) => void,
remove: (id: string) => void,
render: (content: JSX.Element) => void,
toggle: (id: string) => void,
};
const MenuContext = createContext<MenuContext>({} as MenuContext);
export default MenuContext;

View file

@ -0,0 +1,82 @@
// Copyright (C) 2017-2024 Smart code 203358507
import React, { useLayoutEffect, useMemo, useRef, useState } from 'react';
import { createPortal } from 'react-dom';
import classNames from 'classnames';
import useKeyboardEvent from '../useKeyboardEvent';
import MenuContext from './MenuContext';
import { type Menu } from './types';
type Props = {
children: JSX.Element,
className: string,
};
const MenuProvider = ({ children, className }: Props) => {
const container = useRef<HTMLDivElement>(null);
const [menus, setMenus] = useState<Menu[]>([]);
const [style, setStyle] = useState<React.CSSProperties>(null);
const active = useMemo(() => menus.find(({ open }) => open), [menus]);
const create = (menu: Omit<Menu, 'open'>) => {
setMenus((menus) => ([
...menus,
{
...menu,
open: false,
},
]));
};
const remove = (id: string) => {
setMenus((menus) => menus.filter((menu) => menu.id !== id));
};
const render = (content: JSX.Element) => {
return createPortal(content, container.current);
};
const toggle = (id: string) => {
setMenus((menus) => menus.map((menu) => ({
...menu,
open: menu.id === id && !menu.open,
})));
};
const closeAll = () => {
setMenus((menus) => menus.map((menu) => ({
...menu,
open: false,
})));
};
useLayoutEffect(() => {
setStyle(null);
if (container.current && active) {
const menu = container.current.getBoundingClientRect();
const parent = active.parent.getBoundingClientRect();
const y = (parent.top + menu.height) < window.innerHeight ? 'bottom' : 'top';
const x = (parent.left + menu.width) < window.innerWidth ? 'left' : 'right';
setStyle({
top: y === 'top' ? parent.top - menu.height : parent.bottom,
left: (active.align ?? x) === 'left' ? parent.left : parent.right - menu.width,
});
}
}, [active]);
useKeyboardEvent('Escape', closeAll);
return (
<MenuContext.Provider value={{ active, create, remove, render, toggle }}>
{children}
<div ref={container} className={classNames(className, active?.className)} style={style} />
</MenuContext.Provider>
);
};
export default MenuProvider;

9
src/common/Menu/index.ts Normal file
View file

@ -0,0 +1,9 @@
// Copyright (C) 2017-2024 Smart code 203358507
import Menu from './Menu';
import MenuProvider from './MenuProvider';
export {
Menu,
MenuProvider,
};

View file

@ -0,0 +1,11 @@
// Copyright (C) 2017-2023 Smart code 203358507
.menu-placeholder {
z-index: -1;
visibility: hidden;
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
}

7
src/common/Menu/types.d.ts vendored Normal file
View file

@ -0,0 +1,7 @@
export type Menu = {
id: string,
className: string,
parent: HTMLElement,
align: 'left' | 'right',
open: boolean,
};

View file

@ -0,0 +1,8 @@
// Copyright (C) 2017-2024 Smart code 203358507
import { useContext } from 'react';
import MenuContext from './MenuContext';
const useMenu = () => useContext(MenuContext);
export default useMenu;

View file

@ -48,6 +48,7 @@ const EventModal = require('./EventModal');
const { default: useOnClickOutside } = require('./useOnClickOutside');
const { default: useKeyboardEvent } = require('./useKeyboardEvent');
const { default: useMouseEvent } = require('./useMouseEvent');
const { MenuProvider, Menu } = require('./Menu');
module.exports = {
AddonDetailsModal,
@ -102,4 +103,6 @@ module.exports = {
useOnClickOutside,
useKeyboardEvent,
useMouseEvent,
MenuProvider,
Menu,
};

View file

@ -2,17 +2,17 @@
import { useEffect } from 'react';
const useOnClickOutside = (ref: React.MutableRefObject<HTMLElement>, handler: () => void) => {
const useOnClickOutside = (ref: React.MutableRefObject<HTMLElement>, handler: () => void, ignore?: boolean) => {
useEffect(() => {
const onClickOutside = (event: MouseEvent) => {
const element = event.target as Node;
if (ref.current && !ref.current.contains(element)) {
handler();
!ignore && handler();
}
};
document.addEventListener('click', onClickOutside);
return () => document.removeEventListener('click', onClickOutside);
document.addEventListener('mouseup', onClickOutside);
return () => document.removeEventListener('mouseup', onClickOutside);
}, [handler]);
};

2
src/modules.d.ts vendored
View file

@ -1,2 +1,2 @@
declare module '*';
declare module '*.less';
declare module 'stremio/common';

View file

@ -1,9 +1,9 @@
// Copyright (C) 2017-2024 Smart code 203358507
import React, { useCallback, useEffect, useRef } from 'react';
import React from 'react';
import classNames from 'classnames';
import Icon from '@stremio/stremio-icons/react';
import { Button, useBinaryState, useOnClickOutside, useKeyboardEvent } from 'stremio/common';
import { Button } from 'stremio/common';
import styles from './styles.less';
type Props = {
@ -12,53 +12,19 @@ type Props = {
disabled?: boolean,
icon: string,
title?: string,
shortcut?: string,
onMenuChange?: (state: boolean) => void,
onClick?: () => void,
};
const Control = ({ children, className, disabled, icon, title, shortcut, onMenuChange, onClick }: Props) => {
const ref = useRef(null);
const [menuOpen,, closeMenu, toggleMenu] = useBinaryState();
const onButtonClick = () => {
toggleMenu();
onClick && onClick();
};
const onMenuClick = (event: React.MouseEvent<HTMLDivElement>) => {
event.stopPropagation();
};
const onClickOutside = useCallback(() => {
menuOpen && closeMenu();
}, [menuOpen]);
useEffect(() => {
onMenuChange && onMenuChange(menuOpen);
}, [menuOpen, onMenuChange]);
useOnClickOutside(ref, onClickOutside);
useKeyboardEvent('Escape', closeMenu);
shortcut && useKeyboardEvent(shortcut, toggleMenu);
const Control = ({ children, className, disabled, icon, title, onClick }: Props) => {
return (
<Button
ref={ref}
className={classNames(className, styles['control-button'], { 'disabled': disabled })}
tabIndex={-1}
title={title}
onClick={onButtonClick}
onClick={onClick}
>
<Icon className={styles['icon']} name={icon} />
{
children && menuOpen ?
<div className={styles['menu-container']} onClick={onMenuClick}>
{children}
</div>
:
null
}
{children}
</Button>
);
};

View file

@ -22,15 +22,4 @@
height: 2.5rem;
color: var(--primary-foreground-color);
}
.menu-container {
position: absolute;
right: 0;
bottom: 8rem;
border-radius: var(--border-radius);
background-color: var(--modal-background-color);
box-shadow: 0 1.35rem 2.7rem rgba(0, 0, 0, 0.4), 0 1.1rem 0.85rem rgba(0, 0, 0, 0.2);
backdrop-filter: blur(15px);
overflow: hidden;
}
}

View file

@ -31,7 +31,6 @@ const ControlBar = ({
onSubtitlesOffsetChanged,
onSubtitlesSizeChanged,
onExtraSubtitlesDelayChanged,
onMenuChange,
...props
}) => {
const { chromecast } = useServices();
@ -73,14 +72,6 @@ const ControlBar = ({
return streamingServer.playbackDevices !== null && streamingServer.playbackDevices.type === 'Ready' ? streamingServer.playbackDevices.content : [];
}, [streamingServer]);
const onMuteButtonClick = React.useCallback(() => {
muted ? onUnmuteRequested() : onMuteRequested();
}, [muted, onMuteRequested, onUnmuteRequested]);
const onChromecastButtonClick = React.useCallback(() => {
chromecast.transport.requestSession();
}, []);
const volumeIcon = React.useMemo(() => {
return (typeof muted === 'boolean' && muted) ? 'volume-mute' :
(volume === null || isNaN(volume)) ? 'volume-off' :
@ -89,6 +80,14 @@ const ControlBar = ({
'volume-high';
}, [muted, volume]);
const onMuteButtonClick = React.useCallback(() => {
muted ? onUnmuteRequested() : onMuteRequested();
}, [muted, onMuteRequested, onUnmuteRequested]);
const onChromecastButtonClick = React.useCallback(() => {
chromecast.transport.requestSession();
}, []);
React.useEffect(() => {
const onStateChanged = () => setChromecastServiceActive(chromecast.active);
chromecast.on('stateChanged', onStateChanged);
@ -140,16 +139,21 @@ const ControlBar = ({
onClick={toogleMobileMenu}
/>
<div className={classnames(styles['controls-menu-container'], { 'open': mobileMenuOpen })}>
<Control icon={'network'} disabled={!statistics || statistics.infoHash === null || !stream} shortcut={'KeyD'} onMenuChange={onMenuChange}>
<StatisticsMenu {...statistics} />
</Control>
<Control icon={'speed'} disabled={!playbackSpeed} shortcut={'KeyR'} onMenuChange={onMenuChange}>
{
statistics?.infoHash ?
<Control icon={'network'}>
<StatisticsMenu {...statistics} />
</Control>
:
null
}
<Control icon={'speed'} disabled={!playbackSpeed}>
<SpeedMenu
playbackSpeed={playbackSpeed}
onChange={onPlaybackSpeedChangeRequested}
/>
</Control>
<Control icon={'about'} disabled={!metaItem || !stream} shortcut={'KeyI'} onMenuChange={onMenuChange}>
<Control icon={'about'} disabled={!metaItem || !stream}>
<InfoMenu
stream={stream}
addon={addon}
@ -157,7 +161,7 @@ const ControlBar = ({
/>
</Control>
<Control icon={'cast'} disabled={!chromecastServiceActive} onClick={onChromecastButtonClick} />
<Control icon={'subtitles'} disabled={tracks.length === 0} shortcut={'KeyS'} onMenuChange={onMenuChange}>
<Control icon={'subtitles'} disabled={tracks.length === 0}>
<SubtitlesMenu
audioTracks={audioTracks}
selectedAudioTrackId={selectedAudioTrackId}
@ -182,7 +186,7 @@ const ControlBar = ({
</Control>
{
metaItem?.videos?.length > 0 ?
<Control icon={'episodes'} shortcut={'KeyV'} onMenuChange={onMenuChange}>
<Control icon={'episodes'}>
<VideosMenu
metaItem={metaItem}
seriesInfo={seriesInfo}
@ -191,7 +195,7 @@ const ControlBar = ({
:
null
}
<Control icon={'more-horizontal'} onMenuChange={onMenuChange}>
<Control icon={'more-horizontal'}>
<OptionsMenu
stream={stream}
playbackDevices={playbackDevices}

View file

@ -5,7 +5,7 @@ const PropTypes = require('prop-types');
const classnames = require('classnames');
// const Stream = require('stremio/routes/MetaDetails/StreamsList/Stream');
// const AddonDetails = require('stremio/common/AddonDetailsModal/AddonDetails');
const { MetaPreview, CONSTANTS } = require('stremio/common');
const { MetaPreview, Menu, CONSTANTS } = require('stremio/common');
const styles = require('./styles');
const InfoMenu = ({ ...props }) => {
@ -18,11 +18,8 @@ const InfoMenu = ({ ...props }) => {
:
null;
}, [props.metaItem]);
const onMouseDown = React.useCallback((event) => {
event.nativeEvent.infoMenuClosePrevented = true;
}, []);
return (
<div className={classnames(styles['info-menu-container'])} onMouseDown={onMouseDown}>
<Menu className={classnames(styles['info-menu-container'])} shortcut={'KeyI'}>
{
metaItem !== null ?
<MetaPreview
@ -63,7 +60,7 @@ const InfoMenu = ({ ...props }) => {
:
null
} */}
</div>
</Menu>
);
};

View file

@ -4,7 +4,7 @@ const React = require('react');
const PropTypes = require('prop-types');
const classnames = require('classnames');
const { useTranslation } = require('react-i18next');
const { useToast } = require('stremio/common');
const { Menu, useToast } = require('stremio/common');
const { useServices } = require('stremio/services');
const Option = require('./Option');
const styles = require('./styles');
@ -65,11 +65,8 @@ const OptionsMenu = ({ className, stream, playbackDevices }) => {
});
}
}, [streamingUrl]);
const onMouseDown = React.useCallback((event) => {
event.nativeEvent.optionsMenuClosePrevented = true;
}, []);
return (
<div className={classnames(className, styles['options-menu-container'])} onMouseDown={onMouseDown}>
<Menu className={classnames(className, styles['options-menu-container'])}>
{
streamingUrl || downloadUrl ?
<Option
@ -104,7 +101,7 @@ const OptionsMenu = ({ className, stream, playbackDevices }) => {
/>
))
}
</div>
</Menu>
);
};

View file

@ -4,6 +4,7 @@ const React = require('react');
const PropTypes = require('prop-types');
const classnames = require('classnames');
const { useTranslation } = require('react-i18next');
const { Menu } = require('stremio/common');
const Option = require('./Option');
const styles = require('./styles');
@ -12,10 +13,6 @@ const RATES = Array.from(Array(8).keys(), (n) => n * 0.25 + 0.25).reverse();
const SpeedMenu = ({ className, playbackSpeed, onChange }) => {
const { t } = useTranslation();
const onMouseDown = React.useCallback((event) => {
event.nativeEvent.speedMenuClosePrevented = true;
}, []);
const onOptionSelect = React.useCallback((value) => {
if (typeof onChange === 'function') {
onChange(value);
@ -23,7 +20,7 @@ const SpeedMenu = ({ className, playbackSpeed, onChange }) => {
}, [onChange]);
return (
<div className={classnames(className, styles['speed-menu-container'])} onMouseDown={onMouseDown}>
<Menu className={classnames(className, styles['speed-menu-container'])} shortcut={'KeyR'} align={'right'}>
<div className={styles['title']}>
{ t('PLAYBACK_SPEED') }
</div>
@ -40,7 +37,7 @@ const SpeedMenu = ({ className, playbackSpeed, onChange }) => {
))
}
</div>
</div>
</Menu>
);
};

View file

@ -3,11 +3,12 @@
const React = require('react');
const classNames = require('classnames');
const PropTypes = require('prop-types');
const { Menu } = require('stremio/common');
const styles = require('./styles.less');
const StatisticsMenu = ({ className, peers, speed, completed, infoHash }) => {
return (
<div className={classNames(className, styles['statistics-menu-container'])}>
<Menu className={classNames(className, styles['statistics-menu-container'])} shortcut={'KeyD'}>
<div className={styles['title']}>
Statistics
</div>
@ -45,7 +46,7 @@ const StatisticsMenu = ({ className, peers, speed, completed, infoHash }) => {
{ infoHash }
</div>
</div>
</div>
</Menu>
);
};

View file

@ -3,7 +3,7 @@
const React = require('react');
const PropTypes = require('prop-types');
const classnames = require('classnames');
const { Button, CONSTANTS, comparatorWithPriorities, languageNames } = require('stremio/common');
const { Button, Menu, CONSTANTS, comparatorWithPriorities, languageNames } = require('stremio/common');
const DiscreteSelectInput = require('./DiscreteSelectInput');
const styles = require('./styles');
const { t } = require('i18next');
@ -58,9 +58,6 @@ const SubtitlesMenu = React.memo((props) => {
.filter(({ lang }) => lang === selectedSubtitlesLanguage)
.sort((t1, t2) => comparatorWithPriorities(ORIGIN_PRIORITIES)(t1.origin, t2.origin));
}, [props.subtitlesTracks, props.extraSubtitlesTracks, selectedSubtitlesLanguage]);
const onMouseDown = React.useCallback((event) => {
event.nativeEvent.subtitlesMenuClosePrevented = true;
}, []);
const subtitlesLanguageOnClick = React.useCallback((event) => {
const track = (Array.isArray(props.subtitlesTracks) ? props.subtitlesTracks : [])
.concat(Array.isArray(props.extraSubtitlesTracks) ? props.extraSubtitlesTracks : [])
@ -150,7 +147,7 @@ const SubtitlesMenu = React.memo((props) => {
}
}, [props.onAudioTrackSelected]);
return (
<div className={classnames(props.className, styles['subtitles-menu-container'])} onMouseDown={onMouseDown}>
<Menu className={classnames(props.className, styles['subtitles-menu-container'])} shortcut={'KeyS'}>
{
Array.isArray(props.audioTracks) && props.audioTracks.length > 1 ?
<div className={styles['languages-container']}>
@ -278,7 +275,7 @@ const SubtitlesMenu = React.memo((props) => {
onChange={onSubtitlesOffsetChanged}
/>
</div>
</div>
</Menu>
);
});

View file

@ -3,13 +3,11 @@
const React = require('react');
const PropTypes = require('prop-types');
const classnames = require('classnames');
const { Menu } = require('stremio/common');
const Video = require('../../../../MetaDetails/VideosList/Video');
const styles = require('./styles');
const VideosMenu = ({ className, metaItem, seriesInfo }) => {
const onMouseDown = React.useCallback((event) => {
event.nativeEvent.videosMenuClosePrevented = true;
}, []);
const videos = React.useMemo(() => {
return seriesInfo && typeof seriesInfo.season === 'number' && Array.isArray(metaItem.videos) ?
metaItem.videos.filter(({ season }) => season === seriesInfo.season)
@ -17,7 +15,7 @@ const VideosMenu = ({ className, metaItem, seriesInfo }) => {
metaItem.videos;
}, [metaItem, seriesInfo]);
return (
<div className={classnames(className, styles['videos-menu-container'])} onMouseDown={onMouseDown}>
<Menu className={classnames(className, styles['videos-menu-container'])} shortcut={'KeyV'}>
{
videos.map((video, index) => (
<Video
@ -35,7 +33,7 @@ const VideosMenu = ({ className, metaItem, seriesInfo }) => {
/>
))
}
</div>
</Menu>
);
};

View file

@ -8,6 +8,7 @@ const langs = require('langs');
const { useTranslation } = require('react-i18next');
const { useServices } = require('stremio/services');
const { HorizontalNavBar, useFullscreen, useBinaryState, useToast, useStreamingServer, useKeyboardEvent, useMouseEvent, withCoreSuspender } = require('stremio/common');
const { default: useMenu } = require('stremio/common/Menu/useMenu');
const BufferingLoader = require('./BufferingLoader');
const VolumeChangeIndicator = require('./VolumeChangeIndicator');
const Error = require('./Error');
@ -26,6 +27,7 @@ const Player = ({ urlParams, queryParams }) => {
return queryParams.has('forceTranscoding');
}, [queryParams]);
const menu = useMenu();
const [player, videoParamsChanged, timeChanged, pausedChanged, ended, nextVideo] = usePlayer(urlParams);
const [settings, updateSettings] = useSettings();
const streamingServer = useStreamingServer();
@ -40,7 +42,7 @@ const Player = ({ urlParams, queryParams }) => {
const setImmersedDebounced = React.useCallback(debounce(setImmersed, 3000), []);
const [,,, toggleFullscreen] = useFullscreen();
const [areMenusOpen, setMenusState] = React.useState(false);
const areMenusOpen = React.useMemo(() => menu.active, [menu.active]);
const [nextVideoPopupOpen, openNextVideoPopup, closeNextVideoPopup] = useBinaryState(false);
const ignoreShortcuts = React.useMemo(() => {
@ -519,7 +521,6 @@ const Player = ({ urlParams, queryParams }) => {
onSubtitlesOffsetChanged={onSubtitlesOffsetChanged}
onSubtitlesSizeChanged={onSubtitlesSizeChanged}
onExtraSubtitlesDelayChanged={onExtraSubtitlesDelayChanged}
onMenuChange={setMenusState}
onMouseMove={onBarMouseMove}
onMouseOver={onBarMouseMove}
/>