import React, { useCallback, useContext, useLayoutEffect, useRef, useState } from 'react';
import { useSelector } from 'react-redux';
import { useHistory } from 'react-router';
import { colors } from '../../config.json';
import { messages } from '../../shared/gettext';
import useActions from '../lib/actionsHook';
import { useCombinedRefs } from '../lib/utilityHooks';
import { IReduxState } from '../redux/store';
import userInterface from '../redux/userinterface/actions';
import CustomScrollbars, { IScrollEvent } from './CustomScrollbars';
import {
StyledBackBarItemButton,
StyledBackBarItemIcon,
StyledBackBarItemLabel,
StyledCloseBarItemButton,
StyledCloseBarItemIcon,
StyledNavigationBar,
StyledNavigationBarSeparator,
StyledNavigationBarWrapper,
StyledTitleBarItemContainer,
StyledTitleBarItemLabel,
StyledTitleBarItemMeasuringLabel,
} from './NavigationBarStyles';
export { StyledNavigationItems as NavigationItems } from './NavigationBarStyles';
interface INavigationContainerProps {
children?: React.ReactNode;
}
interface INavigationContainerState {
showsBarTitle: boolean;
showsBarSeparator: boolean;
}
const NavigationScrollContext = React.createContext({
showsBarTitle: false,
showsBarSeparator: false,
onScroll(_event: IScrollEvent): void {
throw Error('NavigationScrollContext provider missing');
},
});
export class NavigationContainer extends React.Component<
INavigationContainerProps,
INavigationContainerState
> {
public state = {
showsBarTitle: false,
showsBarSeparator: false,
};
private scrollEventListeners: Array<(event: IScrollEvent) => void> = [];
public componentDidMount() {
this.updateBarAppearance({ scrollLeft: 0, scrollTop: 0 });
}
public render() {
return (
{this.props.children}
);
}
public onScroll = (event: IScrollEvent) => {
this.notifyScrollEventListeners(event);
this.updateBarAppearance(event);
};
public addScrollEventListener(fn: (event: IScrollEvent) => void) {
const index = this.scrollEventListeners.indexOf(fn);
if (index === -1) {
this.scrollEventListeners.push(fn);
}
}
public removeScrollEventListener(fn: (event: IScrollEvent) => void) {
const index = this.scrollEventListeners.indexOf(fn);
if (index !== -1) {
this.scrollEventListeners.splice(index, 1);
}
}
private notifyScrollEventListeners(event: IScrollEvent) {
this.scrollEventListeners.forEach((listener) => listener(event));
}
private updateBarAppearance(event: IScrollEvent) {
// that's where SettingsHeader.HeaderTitle intersects the navigation bar
const showsBarSeparator = event.scrollTop > 11;
// that's when SettingsHeader.HeaderTitle goes behind the navigation bar
const showsBarTitle = event.scrollTop > 20;
if (
this.state.showsBarSeparator !== showsBarSeparator ||
this.state.showsBarTitle !== showsBarTitle
) {
this.setState({ showsBarSeparator, showsBarTitle });
}
}
}
interface INavigationScrollbarsProps {
onScroll?: (value: IScrollEvent) => void;
className?: string;
fillContainer?: boolean;
children?: React.ReactNode;
}
export const NavigationScrollbars = React.forwardRef(function NavigationScrollbarsT(
props: INavigationScrollbarsProps,
forwardedRef?: React.Ref,
) {
const history = useHistory();
const { onScroll } = useContext(NavigationScrollContext);
const ref = useRef();
const combinedRefs = useCombinedRefs(forwardedRef, ref);
const { addScrollPosition, removeScrollPosition } = useActions(userInterface);
const scrollPosition = useSelector(
(state: IReduxState) => state.userInterface.scrollPosition[history.location.pathname],
);
useLayoutEffect(() => {
const path = history.location.pathname;
if (history.action === 'POP' && scrollPosition) {
ref.current?.scrollTo(...scrollPosition);
removeScrollPosition(path);
}
return () => {
if (history.action === 'PUSH' && ref.current) {
addScrollPosition(path, ref.current.getScrollPosition());
}
};
}, []);
const handleScroll = useCallback((event: IScrollEvent) => {
onScroll(event);
props.onScroll?.(event);
}, []);
return (
{props.children}
);
});
const TitleBarItemContext = React.createContext({
titleAdjustment: 0,
visible: false,
get titleContainerRef(): React.RefObject {
throw Error('Missing TitleBarItemContext provider');
},
get measuringTitleRef(): React.RefObject {
throw Error('Missing TitleBarItemContext provider');
},
});
interface INavigationBarProps {
children?: React.ReactNode;
alwaysDisplayBarTitle?: boolean;
}
export const NavigationBar = function NavigationBarT(props: INavigationBarProps) {
const { showsBarSeparator, showsBarTitle } = useContext(NavigationScrollContext);
const unpinnedWindow = useSelector(
(state: IReduxState) => state.settings.guiSettings.unpinnedWindow,
);
const [titleAdjustment, setTitleAdjustment] = useState(0);
const titleContainerRef = useRef() as React.RefObject;
const measuringTitleRef = useRef() as React.RefObject;
const navigationBarRef = useRef() as React.RefObject;
useLayoutEffect(() => {
const titleContainerRect = titleContainerRef.current?.getBoundingClientRect();
const measuringTitleRect = measuringTitleRef.current?.getBoundingClientRect();
const navigationBarRect = navigationBarRef.current?.getBoundingClientRect();
if (titleContainerRect && measuringTitleRect && navigationBarRect) {
// calculate the width of the elements preceding the title view container
const leadingSpace = titleContainerRect.x - navigationBarRect.x;
// calculate the width of the elements succeeding the title view container
const trailingSpace = navigationBarRect.width - titleContainerRect.width - leadingSpace;
// calculate the adjustment needed to center the title view within navigation bar
const titleAdjustment = Math.floor(trailingSpace - leadingSpace);
// calculate the maximum possible adjustment that when applied should keep the text fully
// visible, unless the title container itself is smaller than the space needed to accommodate
// the text
const maxTitleAdjustment = Math.floor(
Math.max(titleContainerRect.width - measuringTitleRect.width, 0),
);
// cap the adjustment to remain within the allowed bounds
const cappedTitleAdjustment = Math.min(
Math.max(-maxTitleAdjustment, titleAdjustment),
maxTitleAdjustment,
);
setTitleAdjustment(cappedTitleAdjustment);
}
});
return (
{props.children}
{showsBarSeparator && }
);
};
interface ITitleBarItemProps {
children?: React.ReactText;
}
export const TitleBarItem = React.memo(function TitleBarItemT(props: ITitleBarItemProps) {
const { measuringTitleRef, titleAdjustment, titleContainerRef, visible } = useContext(
TitleBarItemContext,
);
return (
{props.children}
{props.children}
);
});
interface ICloseBarItemProps {
action: () => void;
}
export function CloseBarItem(props: ICloseBarItemProps) {
// Use the arrow down icon on Linux, to avoid confusion with the close button in the window
// title bar.
const unpinnedWindow = useSelector(
(state: IReduxState) => state.settings.guiSettings.unpinnedWindow,
);
const iconName = unpinnedWindow ? 'icon-close-down' : 'icon-close';
return (
);
}
interface IBackBarItemProps {
children?: React.ReactText;
action: () => void;
}
export function BackBarItem(props: IBackBarItemProps) {
return (
{props.children}
);
}