refactor(useOutsideClick): pass ref as hook param, use ref to calculate context menu offset

This commit is contained in:
Botzy 2025-01-30 13:31:14 +02:00
parent a15ce0ea52
commit e87048799f
4 changed files with 40 additions and 29 deletions

View file

@ -1,11 +1,11 @@
// Copyright (C) 2017-2024 Smart code 203358507
import { useEffect, useRef } from 'react';
const useOutsideClick = (callback: () => void) => {
const ref = useRef<HTMLDivElement>(null);
import { RefObject, useEffect } from 'react';
const useOutsideClick = (ref: RefObject<HTMLDivElement>, callback: () => void) => {
useEffect(() => {
if (!ref?.current) return;
const handleClickOutside = (event: MouseEvent | TouchEvent) => {
if (ref.current && !ref.current.contains(event.target as Node)) {
callback();
@ -19,9 +19,7 @@ const useOutsideClick = (callback: () => void) => {
document.removeEventListener('mousedown', handleClickOutside);
document.removeEventListener('touchstart', handleClickOutside);
};
}, [callback]);
return ref;
}, [ref, callback]);
};
export default useOutsideClick;

View file

@ -1,6 +1,6 @@
// Copyright (C) 2017-2024 Smart code 203358507
import React from 'react';
import React, { useRef } from 'react';
import { Button } from 'stremio/components';
import useBinaryState from 'stremio/common/useBinaryState';
import Dropdown from './Dropdown';
@ -19,13 +19,15 @@ type Props = {
const MultiselectMenu = ({ className, title, options, selectedOption, onSelect }: Props) => {
const [menuOpen, , closeMenu, toggleMenu] = useBinaryState(false);
const multiselectMenuRef = useOutsideClick(() => closeMenu());
const multiselectMenuRef = useRef(null);
const [level, setLevel] = React.useState<number>(0);
const onOptionSelect = (value: number) => {
level ? setLevel(level + 1) : onSelect(value), closeMenu();
};
useOutsideClick(multiselectMenuRef, closeMenu);
return (
<div className={classNames(styles['multiselect-menu'], className)} ref={multiselectMenuRef}>
<Button

View file

@ -10,10 +10,7 @@ const { useServices } = require('stremio/services');
const Option = require('./Option');
const styles = require('./styles');
const OptionsMenu = ({ className, stream, playbackDevices, style, onOutsideClick }) => {
const ref = useOutsideClick(() => {
if (typeof onOutsideClick === 'function') onOutsideClick();
});
const OptionsMenu = ({ menuRef, className, stream, playbackDevices, style, onOutsideClick }) => {
const { t } = useTranslation();
const { core } = useServices();
const platform = usePlatform();
@ -73,8 +70,13 @@ const OptionsMenu = ({ className, stream, playbackDevices, style, onOutsideClick
const onMouseDown = React.useCallback((event) => {
event.nativeEvent.optionsMenuClosePrevented = true;
}, []);
useOutsideClick(menuRef, () => {
if (typeof onOutsideClick === 'function') onOutsideClick();
});
return (
<div ref={ref} style={style} className={classnames(className, styles['options-menu-container'])} onMouseDown={onMouseDown}>
<div ref={menuRef} style={style} className={classnames(className, styles['options-menu-container'])} onMouseDown={onMouseDown}>
{
streamingUrl || downloadUrl ?
<Option
@ -114,6 +116,10 @@ const OptionsMenu = ({ className, stream, playbackDevices, style, onOutsideClick
};
OptionsMenu.propTypes = {
menuRef: PropTypes.oneOfType([
PropTypes.func,
PropTypes.shape({ current: PropTypes.any })
]),
className: PropTypes.string,
stream: PropTypes.object,
playbackDevices: PropTypes.array,

View file

@ -54,6 +54,7 @@ const Player = ({ urlParams, queryParams }) => {
const [immersed, setImmersed] = React.useState(true);
const setImmersedDebounced = React.useCallback(debounce(setImmersed, 3000), []);
const [, , , toggleFullscreen] = useFullscreen();
const [screenWidth, screenHeight] = React.useMemo(() => [window.innerWidth, window.innerHeight], [window]);
const [optionsMenuOpen, , closeOptionsMenu, toggleOptionsMenu] = useBinaryState(false);
const [subtitlesMenuOpen, , closeSubtitlesMenu, toggleSubtitlesMenu] = useBinaryState(false);
@ -64,9 +65,10 @@ const Player = ({ urlParams, queryParams }) => {
const [sideDrawerOpen, , closeSideDrawer, toggleSideDrawer] = useBinaryState(false);
const [contextMenuOpen, openContextMenu, closeContextMenu] = useBinaryState(false);
const [contextCoords, setContextCoords] = React.useState({
x: 0,
y: 0,
x: screenWidth,
y: screenHeight,
});
const contextMenuRef = React.useRef(null);
const menusOpen = React.useMemo(() => {
return optionsMenuOpen || subtitlesMenuOpen || audioMenuOpen || speedMenuOpen || statisticsMenuOpen || sideDrawerOpen || contextMenuOpen;
@ -245,25 +247,27 @@ const Player = ({ urlParams, queryParams }) => {
const onContextMenu = React.useCallback((e) => {
e.preventDefault();
let baseFontSize = 14;
const { clientX, clientY } = event;
const { innerWidth, innerHeight } = window;
const { clientX, clientY } = e;
if (innerWidth > 1600) baseFontSize = 15;
if (innerWidth > 2200) baseFontSize = 16;
const menuWidth = 16 * baseFontSize;
const minMenuHeight = 9 * baseFontSize;
const adjustedX = clientX + menuWidth > innerWidth ? clientX - menuWidth : clientX;
const adjustedY = clientY + minMenuHeight > innerHeight ? clientY - minMenuHeight : clientY;
const menuSize = contextMenuRef?.current?.getBoundingClientRect();
const adjustedX = clientX + menuSize.width > screenWidth ? clientX - menuSize.width : clientX;
const adjustedY = clientY + menuSize.height > screenHeight ? clientY - menuSize.height : clientY;
setContextCoords({
x: adjustedX,
y: adjustedY,
});
openContextMenu();
}, []);
}, [window, contextMenuRef]);
React.useEffect(() => {
if (!contextMenuOpen) {
setContextCoords({
x: screenWidth,
y: screenHeight
});
}
}, [contextMenuOpen]);
const onContainerMouseDown = React.useCallback((event) => {
if (!event.nativeEvent.optionsMenuClosePrevented) {
@ -690,8 +694,9 @@ const Player = ({ urlParams, queryParams }) => {
null
}
{
contextMenuOpen ?
player.selected?.stream ?
<OptionsMenu
menuRef={contextMenuRef}
style={
{
top: `${contextCoords.y}px`,