diff --git a/package-lock.json b/package-lock.json index 2ba4748dc..d31b3ca6b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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, diff --git a/package.json b/package.json index 04beb08e3..bf47969bd 100755 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/App/App.js b/src/App/App.js index b5f67a658..87273c73a 100644 --- a/src/App/App.js +++ b/src/App/App.js @@ -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 = () => { : - - - - + + + + + + : diff --git a/src/App/styles.less b/src/App/styles.less index d0bfc6bd5..3ea9e94a7 100644 --- a/src/App/styles.less +++ b/src/App/styles.less @@ -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%; diff --git a/src/common/Menu/Menu.tsx b/src/common/Menu/Menu.tsx new file mode 100644 index 000000000..15df4d215 --- /dev/null +++ b/src/common/Menu/Menu.tsx @@ -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(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 <> +
+ {menu.active?.id === id.current && menu.render(children)} + ; +}; + +export default Menu; diff --git a/src/common/Menu/MenuContext.ts b/src/common/Menu/MenuContext.ts new file mode 100644 index 000000000..b9a57bf9a --- /dev/null +++ b/src/common/Menu/MenuContext.ts @@ -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) => void, + remove: (id: string) => void, + render: (content: JSX.Element) => void, + toggle: (id: string) => void, +}; + +const MenuContext = createContext({} as MenuContext); + +export default MenuContext; + diff --git a/src/common/Menu/MenuProvider.tsx b/src/common/Menu/MenuProvider.tsx new file mode 100644 index 000000000..dba333e31 --- /dev/null +++ b/src/common/Menu/MenuProvider.tsx @@ -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(null); + + const [menus, setMenus] = useState([]); + const [style, setStyle] = useState(null); + + const active = useMemo(() => menus.find(({ open }) => open), [menus]); + + const create = (menu: Omit) => { + 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 ( + + {children} +
+ + ); +}; + +export default MenuProvider; diff --git a/src/common/Menu/index.ts b/src/common/Menu/index.ts new file mode 100644 index 000000000..0a9dfa55a --- /dev/null +++ b/src/common/Menu/index.ts @@ -0,0 +1,9 @@ +// Copyright (C) 2017-2024 Smart code 203358507 + +import Menu from './Menu'; +import MenuProvider from './MenuProvider'; + +export { + Menu, + MenuProvider, +}; diff --git a/src/common/Menu/styles.less b/src/common/Menu/styles.less new file mode 100644 index 000000000..24ccc90e8 --- /dev/null +++ b/src/common/Menu/styles.less @@ -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; +} \ No newline at end of file diff --git a/src/common/Menu/types.d.ts b/src/common/Menu/types.d.ts new file mode 100644 index 000000000..a2c621dcf --- /dev/null +++ b/src/common/Menu/types.d.ts @@ -0,0 +1,7 @@ +export type Menu = { + id: string, + className: string, + parent: HTMLElement, + align: 'left' | 'right', + open: boolean, +}; \ No newline at end of file diff --git a/src/common/Menu/useMenu.ts b/src/common/Menu/useMenu.ts new file mode 100644 index 000000000..356563c09 --- /dev/null +++ b/src/common/Menu/useMenu.ts @@ -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; diff --git a/src/common/index.js b/src/common/index.js index 099b56c7c..f69b13964 100644 --- a/src/common/index.js +++ b/src/common/index.js @@ -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, }; diff --git a/src/common/useOnClickOutside.tsx b/src/common/useOnClickOutside.tsx index 7acf7dfb0..be37dcd8f 100644 --- a/src/common/useOnClickOutside.tsx +++ b/src/common/useOnClickOutside.tsx @@ -2,17 +2,17 @@ import { useEffect } from 'react'; -const useOnClickOutside = (ref: React.MutableRefObject, handler: () => void) => { +const useOnClickOutside = (ref: React.MutableRefObject, 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]); }; diff --git a/src/modules.d.ts b/src/modules.d.ts index cbf379aae..f1571198c 100644 --- a/src/modules.d.ts +++ b/src/modules.d.ts @@ -1,2 +1,2 @@ -declare module '*'; +declare module '*.less'; declare module 'stremio/common'; \ No newline at end of file diff --git a/src/routes/Player/ControlBar/Control/Control.tsx b/src/routes/Player/ControlBar/Control/Control.tsx index 2fdd747d7..342a65887 100644 --- a/src/routes/Player/ControlBar/Control/Control.tsx +++ b/src/routes/Player/ControlBar/Control/Control.tsx @@ -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) => { - 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 ( ); }; diff --git a/src/routes/Player/ControlBar/Control/styles.less b/src/routes/Player/ControlBar/Control/styles.less index 6b759bba6..fcbf771f0 100644 --- a/src/routes/Player/ControlBar/Control/styles.less +++ b/src/routes/Player/ControlBar/Control/styles.less @@ -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; - } } \ No newline at end of file diff --git a/src/routes/Player/ControlBar/ControlBar.js b/src/routes/Player/ControlBar/ControlBar.js index 13241b8cc..da2cb53eb 100644 --- a/src/routes/Player/ControlBar/ControlBar.js +++ b/src/routes/Player/ControlBar/ControlBar.js @@ -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} />
- - - - + { + statistics?.infoHash ? + + + + : + null + } + - + - + { metaItem?.videos?.length > 0 ? - + + { @@ -18,11 +18,8 @@ const InfoMenu = ({ ...props }) => { : null; }, [props.metaItem]); - const onMouseDown = React.useCallback((event) => { - event.nativeEvent.infoMenuClosePrevented = true; - }, []); return ( -
+ { metaItem !== null ? { : null } */} -
+ ); }; diff --git a/src/routes/Player/ControlBar/Menus/OptionsMenu/OptionsMenu.js b/src/routes/Player/ControlBar/Menus/OptionsMenu/OptionsMenu.js index 6c50e1551..2c1a8e1bb 100644 --- a/src/routes/Player/ControlBar/Menus/OptionsMenu/OptionsMenu.js +++ b/src/routes/Player/ControlBar/Menus/OptionsMenu/OptionsMenu.js @@ -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 ( -
+ { streamingUrl || downloadUrl ?
+ ); }; diff --git a/src/routes/Player/ControlBar/Menus/SpeedMenu/SpeedMenu.js b/src/routes/Player/ControlBar/Menus/SpeedMenu/SpeedMenu.js index 8680ead82..0ee01faac 100644 --- a/src/routes/Player/ControlBar/Menus/SpeedMenu/SpeedMenu.js +++ b/src/routes/Player/ControlBar/Menus/SpeedMenu/SpeedMenu.js @@ -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 ( -
+
{ t('PLAYBACK_SPEED') }
@@ -40,7 +37,7 @@ const SpeedMenu = ({ className, playbackSpeed, onChange }) => { )) }
-
+ ); }; diff --git a/src/routes/Player/ControlBar/Menus/StatisticsMenu/StatisticsMenu.js b/src/routes/Player/ControlBar/Menus/StatisticsMenu/StatisticsMenu.js index 6bab8ecf5..6294ad891 100644 --- a/src/routes/Player/ControlBar/Menus/StatisticsMenu/StatisticsMenu.js +++ b/src/routes/Player/ControlBar/Menus/StatisticsMenu/StatisticsMenu.js @@ -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 ( -
+
Statistics
@@ -45,7 +46,7 @@ const StatisticsMenu = ({ className, peers, speed, completed, infoHash }) => { { infoHash }
-
+ ); }; diff --git a/src/routes/Player/ControlBar/Menus/SubtitlesMenu/SubtitlesMenu.js b/src/routes/Player/ControlBar/Menus/SubtitlesMenu/SubtitlesMenu.js index 0130c58e2..798aa371f 100644 --- a/src/routes/Player/ControlBar/Menus/SubtitlesMenu/SubtitlesMenu.js +++ b/src/routes/Player/ControlBar/Menus/SubtitlesMenu/SubtitlesMenu.js @@ -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 ( -
+ { Array.isArray(props.audioTracks) && props.audioTracks.length > 1 ?
@@ -278,7 +275,7 @@ const SubtitlesMenu = React.memo((props) => { onChange={onSubtitlesOffsetChanged} />
-
+ ); }); diff --git a/src/routes/Player/ControlBar/Menus/VideosMenu/VideosMenu.js b/src/routes/Player/ControlBar/Menus/VideosMenu/VideosMenu.js index ab9634bbc..5b72f3020 100644 --- a/src/routes/Player/ControlBar/Menus/VideosMenu/VideosMenu.js +++ b/src/routes/Player/ControlBar/Menus/VideosMenu/VideosMenu.js @@ -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 ( -
+ { videos.map((video, index) => (
+ ); }; diff --git a/src/routes/Player/Player.js b/src/routes/Player/Player.js index 99a2fab26..66fe9838b 100644 --- a/src/routes/Player/Player.js +++ b/src/routes/Player/Player.js @@ -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} />